From 5217dfd731726aa0c3ce39436b7a5018ff2aa466 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20=C3=81lvarez?= Date: Tue, 10 Dec 2019 16:11:58 +0100 Subject: [PATCH 01/40] update apm index pattern (#52629) --- .../core_plugins/kibana/server/tutorials/apm/index_pattern.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/legacy/core_plugins/kibana/server/tutorials/apm/index_pattern.json b/src/legacy/core_plugins/kibana/server/tutorials/apm/index_pattern.json index 69a165c09c2f9..9001613623ccb 100644 --- a/src/legacy/core_plugins/kibana/server/tutorials/apm/index_pattern.json +++ b/src/legacy/core_plugins/kibana/server/tutorials/apm/index_pattern.json @@ -1,7 +1,7 @@ { "attributes": { "fieldFormatMap": "{\"client.bytes\":{\"id\":\"bytes\"},\"client.nat.port\":{\"id\":\"string\"},\"client.port\":{\"id\":\"string\"},\"destination.bytes\":{\"id\":\"bytes\"},\"destination.nat.port\":{\"id\":\"string\"},\"destination.port\":{\"id\":\"string\"},\"event.duration\":{\"id\":\"duration\",\"params\":{\"inputFormat\":\"nanoseconds\",\"outputFormat\":\"asMilliseconds\",\"outputPrecision\":1}},\"event.sequence\":{\"id\":\"string\"},\"event.severity\":{\"id\":\"string\"},\"http.request.body.bytes\":{\"id\":\"bytes\"},\"http.request.bytes\":{\"id\":\"bytes\"},\"http.response.body.bytes\":{\"id\":\"bytes\"},\"http.response.bytes\":{\"id\":\"bytes\"},\"http.response.status_code\":{\"id\":\"string\"},\"log.syslog.facility.code\":{\"id\":\"string\"},\"log.syslog.priority\":{\"id\":\"string\"},\"network.bytes\":{\"id\":\"bytes\"},\"package.size\":{\"id\":\"string\"},\"process.pgid\":{\"id\":\"string\"},\"process.pid\":{\"id\":\"string\"},\"process.ppid\":{\"id\":\"string\"},\"process.thread.id\":{\"id\":\"string\"},\"server.bytes\":{\"id\":\"bytes\"},\"server.nat.port\":{\"id\":\"string\"},\"server.port\":{\"id\":\"string\"},\"source.bytes\":{\"id\":\"bytes\"},\"source.nat.port\":{\"id\":\"string\"},\"source.port\":{\"id\":\"string\"},\"system.cpu.total.norm.pct\":{\"id\":\"percent\"},\"system.memory.actual.free\":{\"id\":\"bytes\"},\"system.memory.total\":{\"id\":\"bytes\"},\"system.process.cpu.total.norm.pct\":{\"id\":\"percent\"},\"system.process.memory.rss.bytes\":{\"id\":\"bytes\"},\"system.process.memory.size\":{\"id\":\"bytes\"},\"url.port\":{\"id\":\"string\"},\"view spans\":{\"id\":\"url\",\"params\":{\"labelTemplate\":\"View Spans\"}}}", - "fields": "[{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"@timestamp\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"labels\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tags\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.ephemeral_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.account.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.availability_zone\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.instance.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.instance.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.machine.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.provider\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.region\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.image.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.image.tag\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.labels\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.runtime\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.class\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.data\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.ttl\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.header_flags\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.op_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.class\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.subdomain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.resolved_ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.response_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"ecs.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":4,\"doc_values\":true,\"indexed\":true,\"name\":\"error.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.stack_trace\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.action\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.category\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.created\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.dataset\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.duration\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.end\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.kind\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.module\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.outcome\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.provider\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.risk_score\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.risk_score_norm\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.sequence\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.severity\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.start\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.timezone\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.accessed\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.created\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.ctime\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.device\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.directory\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.extension\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.gid\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.group\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.inode\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.mode\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.mtime\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.owner\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.target_path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.uid\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.architecture\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.hostname\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.uptime\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.body.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.body.content\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.method\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.referrer\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.body.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.body.content\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.status_code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.level\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.logger\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.origin.file.line\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.origin.file.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.origin.function\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.facility.code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.facility.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.priority\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.severity.code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.severity.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.application\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.community_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.direction\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.forwarded_ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.iana_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.protocol\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.transport\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.hostname\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.product\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.serial_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.vendor\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"organization.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.architecture\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.checksum\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.install_scope\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.installed\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.license\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.args\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.executable\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pgid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.ppid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.start\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.thread.id\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.thread.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.title\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.uptime\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.working_directory\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"related.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.ephemeral_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.node.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.state\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.framework\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.tactic.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.tactic.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.tactic.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tracing.trace.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tracing.transaction.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.extension\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.fragment\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.password\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.query\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.scheme\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.username\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.device.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.hostname\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"fields\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"timeseries.instance\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.project.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.image.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"docker.container.labels\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.containerized\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.build\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.codename\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.pod.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.pod.uid\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.namespace\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.node.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.labels.*\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.annotations.*\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.replicaset.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.deployment.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.statefulset.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.container.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.container.image\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"processor.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"processor.event\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"timestamp.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"enabled\":false,\"indexed\":false,\"name\":\"http.request.headers\",\"scripted\":false,\"searchable\":false},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.finished\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"enabled\":false,\"indexed\":false,\"name\":\"http.response.headers\",\"scripted\":false,\"searchable\":false},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.environment\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.language.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.language.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.runtime.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.runtime.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.framework.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.framework.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.sampled\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.duration.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.duration.sum.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.self_time.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.self_time.sum.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.breakdown.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.subtype\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.self_time.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.self_time.sum.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"trace.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"parent.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.listening\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.version_major\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.original.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"experimental\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.culprit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.grouping_key\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.module\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":4,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.handled\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.level\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.logger_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.param_message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.cpu.total.norm.pct\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.memory.total\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.memory.actual.free\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.cpu.total.norm.pct\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.memory.size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.memory.rss.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"sourcemap.service.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"sourcemap.service.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"sourcemap.bundle_filepath\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"view spans\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.action\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.start.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.duration.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.sync\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.db.link\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.duration.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.result\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.marks\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.marks.*.*\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.span_count.dropped\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_id\",\"scripted\":false,\"searchable\":false,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_index\",\"scripted\":false,\"searchable\":false,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_score\",\"scripted\":false,\"searchable\":false,\"type\":\"number\"}]", + "fields": "[{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"@timestamp\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"labels\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tags\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.ephemeral_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.account.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.availability_zone\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.instance.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.instance.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.machine.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.provider\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.region\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.image.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.image.tag\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.labels\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.runtime\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.class\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.data\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.ttl\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.header_flags\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.op_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.class\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.subdomain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.resolved_ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.response_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"ecs.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":4,\"doc_values\":true,\"indexed\":true,\"name\":\"error.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.stack_trace\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.action\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.category\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.created\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.dataset\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.duration\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.end\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.kind\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.module\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.outcome\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.provider\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.risk_score\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.risk_score_norm\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.sequence\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.severity\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.start\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.timezone\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.accessed\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.created\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.ctime\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.device\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.directory\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.extension\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.gid\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.group\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.inode\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.mode\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.mtime\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.owner\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.target_path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.uid\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.architecture\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.hostname\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.uptime\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.body.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.body.content\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.method\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.referrer\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.body.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.body.content\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.status_code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.level\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.logger\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.origin.file.line\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.origin.file.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.origin.function\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.facility.code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.facility.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.priority\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.severity.code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.severity.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.application\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.community_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.direction\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.forwarded_ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.iana_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.protocol\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.transport\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.hostname\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.product\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.serial_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.vendor\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"organization.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.architecture\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.checksum\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.install_scope\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.installed\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.license\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.args\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.executable\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pgid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.ppid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.start\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.thread.id\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.thread.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.title\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.uptime\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.working_directory\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"related.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.ephemeral_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.node.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.state\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.framework\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.tactic.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.tactic.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.tactic.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tracing.trace.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tracing.transaction.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.extension\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.fragment\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.password\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.query\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.scheme\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.username\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.device.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.hostname\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"fields\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"timeseries.instance\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.project.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.image.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"docker.container.labels\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.containerized\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.build\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.codename\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.pod.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.pod.uid\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.namespace\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.node.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.labels.*\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.annotations.*\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.replicaset.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.deployment.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.statefulset.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.container.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.container.image\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"processor.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"processor.event\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"timestamp.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"enabled\":false,\"indexed\":false,\"name\":\"http.request.headers\",\"scripted\":false,\"searchable\":false},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.finished\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"enabled\":false,\"indexed\":false,\"name\":\"http.response.headers\",\"scripted\":false,\"searchable\":false},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.environment\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.language.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.language.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.runtime.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.runtime.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.framework.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.framework.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.sampled\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.duration.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.duration.sum.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.self_time.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.self_time.sum.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.breakdown.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.subtype\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.self_time.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.self_time.sum.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"trace.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"parent.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.listening\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.version_major\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.original.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"experimental\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.culprit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.grouping_key\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.module\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":4,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.handled\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.level\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.logger_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.param_message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.cpu.total.norm.pct\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.memory.total\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.memory.actual.free\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.cpu.total.norm.pct\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.memory.size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.memory.rss.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.cpu.ns\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.samples.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.alloc_objects.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.alloc_space.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.inuse_objects.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.inuse_space.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.duration\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.top.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.top.function\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.top.filename\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.top.line\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.stack.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.stack.function\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.stack.filename\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.stack.line\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"sourcemap.service.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"sourcemap.service.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"sourcemap.bundle_filepath\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"view spans\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.action\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.start.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.duration.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.sync\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.db.link\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.duration.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.result\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.marks\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.marks.*.*\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.span_count.dropped\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_id\",\"scripted\":false,\"searchable\":false,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_index\",\"scripted\":false,\"searchable\":false,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_score\",\"scripted\":false,\"searchable\":false,\"type\":\"number\"}]", "sourceFilters": "[{\"value\":\"sourcemap.sourcemap\"}]", "timeFieldName": "@timestamp" }, From 6ea07cbb9cfa01bc428da29c2d2beedb73594f8a Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Tue, 10 Dec 2019 08:43:04 -0700 Subject: [PATCH 02/40] [SIEM][Detection Engine] Renaming and moving of folders and files (#52587) ## Summary * Creates several folders * Moves schema into smaller files * Moves `utils.ts` in smaller files * Splits apart the types to not be in one giant file but rather cascade bottom up ### Checklist Use ~~strikethroughs~~ to remove checklist items you don't feel are applicable to this PR. ~~- [ ] This was checked for cross-browser compatibility, [including a check against IE11](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility)~~ ~~- [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/master/packages/kbn-i18n/README.md)~~ ~~- [ ] [Documentation](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#writing-documentation) was added for features that require explanation or tutorials~~ - [x] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios ~~- [ ] This was checked for [keyboard-only and screenreader accessibility](https://developer.mozilla.org/en-US/docs/Learn/Tools_and_testing/Cross_browser_testing/Accessibility#Accessibility_testing_checklist)~~ ### For maintainers ~~- [ ] This was checked for breaking API changes and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process)~~ - [x] This includes a feature addition or change that requires a release note and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process) --- .../plugins/siem/server/kibana.index.ts | 16 +- .../lib/detection_engine/alerts/types.ts | 264 -- .../lib/detection_engine/alerts/utils.test.ts | 1107 --------- .../lib/detection_engine/alerts/utils.ts | 408 ---- .../routes/__mocks__/request_responses.ts | 4 +- .../{ => rules}/create_rules_route.test.ts | 6 +- .../routes/{ => rules}/create_rules_route.ts | 17 +- .../{ => rules}/delete_rules_route.test.ts | 6 +- .../routes/{ => rules}/delete_rules_route.ts | 13 +- .../{ => rules}/find_rules_route.test.ts | 6 +- .../routes/{ => rules}/find_rules_route.ts | 13 +- .../{ => rules}/read_rules_route.test.ts | 6 +- .../routes/{ => rules}/read_rules_route.ts | 13 +- .../detection_engine/routes/rules/types.ts | 11 + .../{ => rules}/update_rules_route.test.ts | 6 +- .../routes/{ => rules}/update_rules_route.ts | 13 +- .../routes/rules/utils.test.ts | 496 ++++ .../detection_engine/routes/rules/utils.ts | 76 + .../detection_engine/routes/schemas.test.ts | 2133 ----------------- .../lib/detection_engine/routes/schemas.ts | 162 -- .../schemas/create_rules_schema.test.ts | 1047 ++++++++ .../routes/schemas/create_rules_schema.ts | 67 + .../routes/schemas/find_rules_schema.test.ts | 136 ++ .../routes/schemas/find_rules_schema.ts | 24 + .../routes/schemas/query_rules_schema.test.ts | 32 + .../routes/schemas/query_rules_schema.ts | 16 + .../routes/schemas/schemas.ts | 79 + .../schemas/set_signal_status_schema.test.ts | 66 + .../schemas/set_signal_status_schema.ts | 17 + .../schemas/update_rules_schema.test.ts | 869 +++++++ .../routes/schemas/update_rules_schema.ts | 63 + .../signals/open_close_signals_route.ts | 4 +- .../lib/detection_engine/routes/utils.test.ts | 488 +--- .../lib/detection_engine/routes/utils.ts | 68 - .../{alerts => rules}/create_rules.ts | 0 .../{alerts => rules}/delete_rules.ts | 0 .../{alerts => rules}/find_rules.test.ts | 0 .../{alerts => rules}/find_rules.ts | 0 .../{alerts => rules}/read_rules.test.ts | 0 .../{alerts => rules}/read_rules.ts | 0 .../lib/detection_engine/rules/types.ts | 102 + .../{alerts => rules}/update_rules.test.ts | 0 .../{alerts => rules}/update_rules.ts | 0 .../__mocks__/es_results.ts | 19 +- .../signals/build_bulk_body.test.ts | 284 +++ .../signals/build_bulk_body.ts | 56 + .../signals/build_event_type_signal.test.ts | 47 + .../signals/build_event_type_signal.ts | 15 + .../build_events_query.test.ts | 0 .../{alerts => signals}/build_events_query.ts | 0 .../signals/build_rule.test.ts | 156 ++ .../detection_engine/signals/build_rule.ts | 59 + .../signals/build_signal.test.ts | 111 + .../detection_engine/signals/build_signal.ts | 26 + .../{alerts => signals}/get_filter.test.ts | 2 +- .../{alerts => signals}/get_filter.ts | 2 +- .../get_input_output_index.test.ts | 0 .../get_input_output_index.ts | 0 .../signals/search_after_bulk_create.test.ts | 286 +++ .../signals/search_after_bulk_create.ts | 135 ++ .../signal_rule_alert_type.ts} | 10 +- .../signals/single_bulk_create.test.ts | 230 ++ .../signals/single_bulk_create.ts | 106 + .../signals/single_search_after.test.ts | 73 + .../signals/single_search_after.ts | 52 + .../lib/detection_engine/signals/types.ts | 123 + .../lib/detection_engine/signals/utils.ts | 16 + .../siem/server/lib/detection_engine/types.ts | 67 + .../legacy/plugins/siem/server/lib/types.ts | 20 - 69 files changed, 5029 insertions(+), 4720 deletions(-) delete mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/types.ts delete mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.test.ts delete mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.ts rename x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/{ => rules}/create_rules_route.test.ts (96%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/{ => rules}/create_rules_route.ts (85%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/{ => rules}/delete_rules_route.test.ts (96%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/{ => rules}/delete_rules_route.ts (78%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/{ => rules}/find_rules_route.test.ts (94%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/{ => rules}/find_rules_route.ts (78%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/{ => rules}/read_rules_route.test.ts (94%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/{ => rules}/read_rules_route.ts (78%) create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/types.ts rename x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/{ => rules}/update_rules_route.test.ts (97%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/{ => rules}/update_rules_route.ts (84%) create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts delete mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.test.ts delete mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.test.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/find_rules_schema.test.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/find_rules_schema.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_rules_schema.test.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_rules_schema.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/schemas.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/set_signal_status_schema.test.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/set_signal_status_schema.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.test.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.ts rename x-pack/legacy/plugins/siem/server/lib/detection_engine/{alerts => rules}/create_rules.ts (100%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/{alerts => rules}/delete_rules.ts (100%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/{alerts => rules}/find_rules.test.ts (100%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/{alerts => rules}/find_rules.ts (100%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/{alerts => rules}/read_rules.test.ts (100%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/{alerts => rules}/read_rules.ts (100%) create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts rename x-pack/legacy/plugins/siem/server/lib/detection_engine/{alerts => rules}/update_rules.test.ts (100%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/{alerts => rules}/update_rules.ts (100%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/{alerts => signals}/__mocks__/es_results.ts (94%) create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_bulk_body.test.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_bulk_body.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_event_type_signal.test.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_event_type_signal.ts rename x-pack/legacy/plugins/siem/server/lib/detection_engine/{alerts => signals}/build_events_query.test.ts (100%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/{alerts => signals}/build_events_query.ts (100%) create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_rule.test.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_rule.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_signal.test.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_signal.ts rename x-pack/legacy/plugins/siem/server/lib/detection_engine/{alerts => signals}/get_filter.test.ts (99%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/{alerts => signals}/get_filter.ts (98%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/{alerts => signals}/get_input_output_index.test.ts (100%) rename x-pack/legacy/plugins/siem/server/lib/detection_engine/{alerts => signals}/get_input_output_index.ts (100%) create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.test.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.ts rename x-pack/legacy/plugins/siem/server/lib/detection_engine/{alerts/rules_alert_type.ts => signals/signal_rule_alert_type.ts} (96%) create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.test.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_search_after.test.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_search_after.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/types.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/utils.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts diff --git a/x-pack/legacy/plugins/siem/server/kibana.index.ts b/x-pack/legacy/plugins/siem/server/kibana.index.ts index bb0958b32fa19..f56e6b3c3f550 100644 --- a/x-pack/legacy/plugins/siem/server/kibana.index.ts +++ b/x-pack/legacy/plugins/siem/server/kibana.index.ts @@ -6,18 +6,18 @@ import { PluginInitializerContext } from 'src/core/server'; -import { rulesAlertType } from './lib/detection_engine/alerts/rules_alert_type'; -import { isAlertExecutor } from './lib/detection_engine/alerts/types'; -import { createRulesRoute } from './lib/detection_engine/routes/create_rules_route'; +import { signalRulesAlertType } from './lib/detection_engine/signals/signal_rule_alert_type'; +import { createRulesRoute } from './lib/detection_engine/routes/rules/create_rules_route'; import { createIndexRoute } from './lib/detection_engine/routes/index/create_index_route'; import { readIndexRoute } from './lib/detection_engine/routes/index/read_index_route'; -import { readRulesRoute } from './lib/detection_engine/routes/read_rules_route'; -import { findRulesRoute } from './lib/detection_engine/routes/find_rules_route'; -import { deleteRulesRoute } from './lib/detection_engine/routes/delete_rules_route'; -import { updateRulesRoute } from './lib/detection_engine/routes/update_rules_route'; +import { readRulesRoute } from './lib/detection_engine/routes/rules/read_rules_route'; +import { findRulesRoute } from './lib/detection_engine/routes/rules/find_rules_route'; +import { deleteRulesRoute } from './lib/detection_engine/routes/rules/delete_rules_route'; +import { updateRulesRoute } from './lib/detection_engine/routes/rules/update_rules_route'; import { setSignalsStatusRoute } from './lib/detection_engine/routes/signals/open_close_signals_route'; import { ServerFacade } from './types'; import { deleteIndexRoute } from './lib/detection_engine/routes/index/delete_index_route'; +import { isAlertExecutor } from './lib/detection_engine/signals/types'; const APP_ID = 'siem'; @@ -26,7 +26,7 @@ export const initServerWithKibana = (context: PluginInitializerContext, __legacy const version = context.env.packageInfo.version; if (__legacy.plugins.alerting != null) { - const type = rulesAlertType({ logger, version }); + const type = signalRulesAlertType({ logger, version }); if (isAlertExecutor(type)) { __legacy.plugins.alerting.setup.registerType(type); } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/types.ts deleted file mode 100644 index c9d265ebffacd..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/types.ts +++ /dev/null @@ -1,264 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { get } from 'lodash/fp'; - -import { SIGNALS_ID } from '../../../../common/constants'; -import { - Alert, - AlertType, - State, - AlertExecutorOptions, -} from '../../../../../alerting/server/types'; -import { AlertsClient } from '../../../../../alerting/server/alerts_client'; -import { ActionsClient } from '../../../../../actions/server/actions_client'; -import { RequestFacade } from '../../../types'; -import { SearchResponse } from '../../types'; -import { esFilters } from '../../../../../../../../src/plugins/data/server'; - -export type PartialFilter = Partial; - -export interface IMitreAttack { - id: string; - name: string; - reference: string; -} -export interface ThreatParams { - framework: string; - tactic: IMitreAttack; - techniques: IMitreAttack[]; -} -export interface RuleAlertParams { - description: string; - enabled: boolean; - falsePositives: string[]; - filters: PartialFilter[] | undefined | null; - from: string; - immutable: boolean; - index: string[]; - interval: string; - ruleId: string | undefined | null; - language: string | undefined | null; - maxSignals: number; - riskScore: number; - outputIndex: string; - name: string; - query: string | undefined | null; - references: string[]; - savedId: string | undefined | null; - meta: Record | undefined | null; - severity: string; - tags: string[]; - to: string; - threats: ThreatParams[] | undefined | null; - type: 'query' | 'saved_query'; -} - -export type RuleAlertParamsRest = Omit< - RuleAlertParams, - 'ruleId' | 'falsePositives' | 'maxSignals' | 'savedId' | 'riskScore' | 'outputIndex' -> & { - rule_id: RuleAlertParams['ruleId']; - false_positives: RuleAlertParams['falsePositives']; - saved_id: RuleAlertParams['savedId']; - max_signals: RuleAlertParams['maxSignals']; - risk_score: RuleAlertParams['riskScore']; - output_index: RuleAlertParams['outputIndex']; -}; - -export interface SignalsParams { - signalIds: string[] | undefined | null; - query: object | undefined | null; - status: 'open' | 'closed'; -} - -export type SignalsRestParams = Omit & { - signal_ids: SignalsParams['signalIds']; -}; - -export type OutputRuleAlertRest = RuleAlertParamsRest & { - id: string; - created_by: string | undefined | null; - updated_by: string | undefined | null; -}; - -export type UpdateRuleAlertParamsRest = Partial & { - id: string | undefined; - rule_id: RuleAlertParams['ruleId'] | undefined; -}; - -export interface FindParamsRest { - per_page: number; - page: number; - sort_field: string; - sort_order: 'asc' | 'desc'; - fields: string[]; - filter: string; -} - -export interface Clients { - alertsClient: AlertsClient; - actionsClient: ActionsClient; -} - -export type RuleParams = RuleAlertParams & Clients; - -export type UpdateRuleParams = Partial & { - id: string | undefined | null; -} & Clients; - -export type DeleteRuleParams = Clients & { - id: string | undefined; - ruleId: string | undefined | null; -}; - -export interface FindRulesRequest extends Omit { - query: { - per_page: number; - page: number; - search?: string; - sort_field?: string; - filter?: string; - fields?: string[]; - sort_order?: 'asc' | 'desc'; - }; -} - -export interface FindRuleParams { - alertsClient: AlertsClient; - perPage?: number; - page?: number; - sortField?: string; - filter?: string; - fields?: string[]; - sortOrder?: 'asc' | 'desc'; -} - -export interface ReadRuleParams { - alertsClient: AlertsClient; - id?: string | undefined | null; - ruleId?: string | undefined | null; -} - -export interface ReadRuleByRuleId { - alertsClient: AlertsClient; - ruleId: string; -} - -export type RuleTypeParams = Omit; - -export type RuleAlertType = Alert & { - id: string; - params: RuleTypeParams; -}; - -export interface RulesRequest extends RequestFacade { - payload: RuleAlertParamsRest; -} - -export interface SignalsRequest extends RequestFacade { - payload: SignalsRestParams; -} - -export interface UpdateRulesRequest extends RequestFacade { - payload: UpdateRuleAlertParamsRest; -} - -export type RuleExecutorOptions = Omit & { - params: RuleAlertParams & { - scrollSize: number; - scrollLock: string; - }; -}; - -export type SearchTypes = - | string - | string[] - | number - | number[] - | boolean - | boolean[] - | object - | object[]; - -export interface SignalSource { - [key: string]: SearchTypes; - '@timestamp': string; -} - -export interface BulkResponse { - took: number; - errors: boolean; - items: [ - { - create: { - _index: string; - _type?: string; - _id: string; - _version: number; - result?: string; - _shards?: { - total: number; - successful: number; - failed: number; - }; - _seq_no?: number; - _primary_term?: number; - status: number; - error?: { - type: string; - reason: string; - index_uuid?: string; - shard: string; - index: string; - }; - }; - } - ]; -} - -export interface MGetResponse { - docs: GetResponse[]; -} -export interface GetResponse { - _index: string; - _type: string; - _id: string; - _version: number; - _seq_no: number; - _primary_term: number; - found: boolean; - _source: SearchTypes; -} - -export type SignalSearchResponse = SearchResponse; -export type SignalSourceHit = SignalSearchResponse['hits']['hits'][0]; - -export type QueryRequest = Omit & { - query: { id: string | undefined; rule_id: string | undefined }; -}; - -// This returns true because by default a RuleAlertTypeDefinition is an AlertType -// since we are only increasing the strictness of params. -export const isAlertExecutor = (obj: RuleAlertTypeDefinition): obj is AlertType => { - return true; -}; - -export type RuleAlertTypeDefinition = Omit & { - executor: ({ services, params, state }: RuleExecutorOptions) => Promise; -}; - -export const isAlertTypes = (obj: unknown[]): obj is RuleAlertType[] => { - return obj.every(rule => isAlertType(rule)); -}; - -export const isAlertType = (obj: unknown): obj is RuleAlertType => { - return get('alertTypeId', obj) === SIGNALS_ID; -}; - -export const isAlertTypeArray = (objArray: unknown[]): objArray is RuleAlertType[] => { - return objArray.length === 0 || isAlertType(objArray[0]); -}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.test.ts deleted file mode 100644 index 41052ab4bbb15..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.test.ts +++ /dev/null @@ -1,1107 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import uuid from 'uuid'; -import { savedObjectsClientMock } from 'src/core/server/mocks'; - -import { Logger } from '../../../../../../../../src/core/server'; -import { - buildBulkBody, - generateId, - singleBulkCreate, - singleSearchAfter, - searchAfterAndBulkCreate, - buildEventTypeSignal, - buildSignal, - buildRule, -} from './utils'; -import { - sampleDocNoSortId, - sampleRuleAlertParams, - sampleDocSearchResultsNoSortId, - sampleDocSearchResultsNoSortIdNoHits, - sampleDocSearchResultsNoSortIdNoVersion, - sampleDocSearchResultsWithSortId, - sampleEmptyDocSearchResults, - repeatedSearchResultsWithSortId, - sampleBulkCreateDuplicateResult, - sampleRuleGuid, - sampleRule, - sampleIdGuid, -} from './__mocks__/es_results'; -import { DEFAULT_SIGNALS_INDEX } from '../../../../common/constants'; -import { OutputRuleAlertRest } from './types'; -import { Signal } from '../../types'; - -const mockLogger: Logger = { - log: jest.fn(), - trace: jest.fn(), - debug: jest.fn(), - info: jest.fn(), - warn: jest.fn(), - error: jest.fn(), - fatal: jest.fn(), -}; - -const mockService = { - callCluster: jest.fn(), - alertInstanceFactory: jest.fn(), - savedObjectsClient: savedObjectsClientMock.create(), -}; - -describe('utils', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - describe('buildBulkBody', () => { - test('if bulk body builds well-defined body', () => { - const sampleParams = sampleRuleAlertParams(); - const fakeSignalSourceHit = buildBulkBody({ - doc: sampleDocNoSortId(), - ruleParams: sampleParams, - id: sampleRuleGuid, - name: 'rule-name', - createdBy: 'elastic', - updatedBy: 'elastic', - interval: '5m', - enabled: true, - tags: ['some fake tag 1', 'some fake tag 2'], - }); - // Timestamp will potentially always be different so remove it for the test - delete fakeSignalSourceHit['@timestamp']; - expect(fakeSignalSourceHit).toEqual({ - someKey: 'someValue', - event: { - kind: 'signal', - }, - signal: { - parent: { - id: sampleIdGuid, - type: 'event', - index: 'myFakeSignalIndex', - depth: 1, - }, - original_time: 'someTimeStamp', - status: 'open', - rule: { - id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', - rule_id: 'rule-1', - false_positives: [], - max_signals: 10000, - risk_score: 50, - output_index: '.siem-signals', - description: 'Detecting root and admin users', - from: 'now-6m', - immutable: false, - index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - interval: '5m', - language: 'kuery', - name: 'rule-name', - query: 'user.name: root or user.name: admin', - references: ['http://google.com'], - severity: 'high', - tags: ['some fake tag 1', 'some fake tag 2'], - type: 'query', - to: 'now', - enabled: true, - created_by: 'elastic', - updated_by: 'elastic', - }, - }, - }); - }); - - test('if bulk body builds original_event if it exists on the event to begin with', () => { - const sampleParams = sampleRuleAlertParams(); - const doc = sampleDocNoSortId(); - doc._source.event = { - action: 'socket_opened', - module: 'system', - dataset: 'socket', - kind: 'event', - }; - const fakeSignalSourceHit = buildBulkBody({ - doc, - ruleParams: sampleParams, - id: sampleRuleGuid, - name: 'rule-name', - createdBy: 'elastic', - updatedBy: 'elastic', - interval: '5m', - enabled: true, - tags: ['some fake tag 1', 'some fake tag 2'], - }); - // Timestamp will potentially always be different so remove it for the test - delete fakeSignalSourceHit['@timestamp']; - expect(fakeSignalSourceHit).toEqual({ - someKey: 'someValue', - event: { - action: 'socket_opened', - dataset: 'socket', - kind: 'signal', - module: 'system', - }, - signal: { - original_event: { - action: 'socket_opened', - dataset: 'socket', - kind: 'event', - module: 'system', - }, - parent: { - id: sampleIdGuid, - type: 'event', - index: 'myFakeSignalIndex', - depth: 1, - }, - original_time: 'someTimeStamp', - status: 'open', - rule: { - id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', - rule_id: 'rule-1', - false_positives: [], - max_signals: 10000, - risk_score: 50, - output_index: '.siem-signals', - description: 'Detecting root and admin users', - from: 'now-6m', - immutable: false, - index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - interval: '5m', - language: 'kuery', - name: 'rule-name', - query: 'user.name: root or user.name: admin', - references: ['http://google.com'], - severity: 'high', - tags: ['some fake tag 1', 'some fake tag 2'], - type: 'query', - to: 'now', - enabled: true, - created_by: 'elastic', - updated_by: 'elastic', - }, - }, - }); - }); - - test('if bulk body builds original_event if it exists on the event to begin with but no kind information', () => { - const sampleParams = sampleRuleAlertParams(); - const doc = sampleDocNoSortId(); - doc._source.event = { - action: 'socket_opened', - module: 'system', - dataset: 'socket', - }; - const fakeSignalSourceHit = buildBulkBody({ - doc, - ruleParams: sampleParams, - id: sampleRuleGuid, - name: 'rule-name', - createdBy: 'elastic', - updatedBy: 'elastic', - interval: '5m', - enabled: true, - tags: ['some fake tag 1', 'some fake tag 2'], - }); - // Timestamp will potentially always be different so remove it for the test - delete fakeSignalSourceHit['@timestamp']; - expect(fakeSignalSourceHit).toEqual({ - someKey: 'someValue', - event: { - action: 'socket_opened', - dataset: 'socket', - kind: 'signal', - module: 'system', - }, - signal: { - original_event: { - action: 'socket_opened', - dataset: 'socket', - module: 'system', - }, - parent: { - id: sampleIdGuid, - type: 'event', - index: 'myFakeSignalIndex', - depth: 1, - }, - original_time: 'someTimeStamp', - status: 'open', - rule: { - id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', - rule_id: 'rule-1', - false_positives: [], - max_signals: 10000, - risk_score: 50, - output_index: '.siem-signals', - description: 'Detecting root and admin users', - from: 'now-6m', - immutable: false, - index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - interval: '5m', - language: 'kuery', - name: 'rule-name', - query: 'user.name: root or user.name: admin', - references: ['http://google.com'], - severity: 'high', - tags: ['some fake tag 1', 'some fake tag 2'], - type: 'query', - to: 'now', - enabled: true, - created_by: 'elastic', - updated_by: 'elastic', - }, - }, - }); - }); - - test('if bulk body builds original_event if it exists on the event to begin with with only kind information', () => { - const sampleParams = sampleRuleAlertParams(); - const doc = sampleDocNoSortId(); - doc._source.event = { - kind: 'event', - }; - const fakeSignalSourceHit = buildBulkBody({ - doc, - ruleParams: sampleParams, - id: sampleRuleGuid, - name: 'rule-name', - createdBy: 'elastic', - updatedBy: 'elastic', - interval: '5m', - enabled: true, - tags: ['some fake tag 1', 'some fake tag 2'], - }); - // Timestamp will potentially always be different so remove it for the test - delete fakeSignalSourceHit['@timestamp']; - expect(fakeSignalSourceHit).toEqual({ - someKey: 'someValue', - event: { - kind: 'signal', - }, - signal: { - original_event: { - kind: 'event', - }, - parent: { - id: sampleIdGuid, - type: 'event', - index: 'myFakeSignalIndex', - depth: 1, - }, - original_time: 'someTimeStamp', - status: 'open', - rule: { - id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', - rule_id: 'rule-1', - false_positives: [], - max_signals: 10000, - risk_score: 50, - output_index: '.siem-signals', - description: 'Detecting root and admin users', - from: 'now-6m', - immutable: false, - index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - interval: '5m', - language: 'kuery', - name: 'rule-name', - query: 'user.name: root or user.name: admin', - references: ['http://google.com'], - severity: 'high', - tags: ['some fake tag 1', 'some fake tag 2'], - type: 'query', - to: 'now', - enabled: true, - created_by: 'elastic', - updated_by: 'elastic', - }, - }, - }); - }); - }); - describe('singleBulkCreate', () => { - describe('create signal id gereateId', () => { - test('two docs with same index, id, and version should have same id', () => { - const findex = 'myfakeindex'; - const fid = 'somefakeid'; - const version = '1'; - const ruleId = 'rule-1'; - // 'myfakeindexsomefakeid1rule-1' - const generatedHash = '342404d620be4344d6d90dd0461d1d1848aec457944d5c5f40cc0cbfedb36679'; - const firstHash = generateId(findex, fid, version, ruleId); - const secondHash = generateId(findex, fid, version, ruleId); - expect(firstHash).toEqual(generatedHash); - expect(secondHash).toEqual(generatedHash); - expect(Buffer.byteLength(firstHash, 'utf8')).toBeLessThan(512); // 512 bytes is maximum size of _id field - expect(Buffer.byteLength(secondHash, 'utf8')).toBeLessThan(512); - }); - test('two docs with different index, id, and version should have different id', () => { - const findex = 'myfakeindex'; - const findex2 = 'mysecondfakeindex'; - const fid = 'somefakeid'; - const version = '1'; - const ruleId = 'rule-1'; - // 'myfakeindexsomefakeid1rule-1' - const firstGeneratedHash = - '342404d620be4344d6d90dd0461d1d1848aec457944d5c5f40cc0cbfedb36679'; - // 'mysecondfakeindexsomefakeid1rule-1' - const secondGeneratedHash = - 'a852941273f805ffe9006e574601acc8ae1148d6c0b3f7f8c4785cba8f6b768a'; - const firstHash = generateId(findex, fid, version, ruleId); - const secondHash = generateId(findex2, fid, version, ruleId); - expect(firstHash).toEqual(firstGeneratedHash); - expect(secondHash).toEqual(secondGeneratedHash); - expect(Buffer.byteLength(firstHash, 'utf8')).toBeLessThan(512); // 512 bytes is maximum size of _id field - expect(Buffer.byteLength(secondHash, 'utf8')).toBeLessThan(512); - expect(firstHash).not.toEqual(secondHash); - }); - test('two docs with same index, different id, and same version should have different id', () => { - const findex = 'myfakeindex'; - const fid = 'somefakeid'; - const fid2 = 'somefakeid2'; - const version = '1'; - const ruleId = 'rule-1'; - // 'myfakeindexsomefakeid1rule-1' - const firstGeneratedHash = - '342404d620be4344d6d90dd0461d1d1848aec457944d5c5f40cc0cbfedb36679'; - // 'myfakeindexsomefakeid21rule-1' - const secondGeneratedHash = - '7d33faea18159fd010c4b79890620e8b12cdc88ec1d370149d0e5552ce860255'; - const firstHash = generateId(findex, fid, version, ruleId); - const secondHash = generateId(findex, fid2, version, ruleId); - expect(firstHash).toEqual(firstGeneratedHash); - expect(secondHash).toEqual(secondGeneratedHash); - expect(Buffer.byteLength(firstHash, 'utf8')).toBeLessThan(512); // 512 bytes is maximum size of _id field - expect(Buffer.byteLength(secondHash, 'utf8')).toBeLessThan(512); - expect(firstHash).not.toEqual(secondHash); - }); - test('two docs with same index, same id, and different version should have different id', () => { - const findex = 'myfakeindex'; - const fid = 'somefakeid'; - const version = '1'; - const version2 = '2'; - const ruleId = 'rule-1'; - // 'myfakeindexsomefakeid1rule-1' - const firstGeneratedHash = - '342404d620be4344d6d90dd0461d1d1848aec457944d5c5f40cc0cbfedb36679'; - // myfakeindexsomefakeid2rule-1' - const secondGeneratedHash = - 'f016f3071fa9df9221d2fb2ba92389d4d388a4347c6ec7a4012c01cb1c640a40'; - const firstHash = generateId(findex, fid, version, ruleId); - const secondHash = generateId(findex, fid, version2, ruleId); - expect(firstHash).toEqual(firstGeneratedHash); - expect(secondHash).toEqual(secondGeneratedHash); - expect(Buffer.byteLength(firstHash, 'utf8')).toBeLessThan(512); // 512 bytes is maximum size of _id field - expect(Buffer.byteLength(secondHash, 'utf8')).toBeLessThan(512); - expect(firstHash).not.toEqual(secondHash); - }); - test('Ensure generated id is less than 512 bytes, even for really really long strings', () => { - const longIndexName = - 'myfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindex'; - const fid = 'somefakeid'; - const version = '1'; - const ruleId = 'rule-1'; - const firstHash = generateId(longIndexName, fid, version, ruleId); - expect(Buffer.byteLength(firstHash, 'utf8')).toBeLessThan(512); // 512 bytes is maximum size of _id field - }); - test('two docs with same index, same id, same version number, and different rule ids should have different id', () => { - const findex = 'myfakeindex'; - const fid = 'somefakeid'; - const version = '1'; - const ruleId = 'rule-1'; - const ruleId2 = 'rule-2'; - // 'myfakeindexsomefakeid1rule-1' - const firstGeneratedHash = - '342404d620be4344d6d90dd0461d1d1848aec457944d5c5f40cc0cbfedb36679'; - // myfakeindexsomefakeid1rule-2' - const secondGeneratedHash = - '1eb04f997086f8b3b143d4d9b18ac178c4a7423f71a5dad9ba8b9e92603c6863'; - const firstHash = generateId(findex, fid, version, ruleId); - const secondHash = generateId(findex, fid, version, ruleId2); - expect(firstHash).toEqual(firstGeneratedHash); - expect(secondHash).toEqual(secondGeneratedHash); - expect(Buffer.byteLength(firstHash, 'utf8')).toBeLessThan(512); // 512 bytes is maximum size of _id field - expect(Buffer.byteLength(secondHash, 'utf8')).toBeLessThan(512); - expect(firstHash).not.toEqual(secondHash); - }); - }); - test('create successful bulk create', async () => { - const sampleParams = sampleRuleAlertParams(); - const sampleSearchResult = sampleDocSearchResultsNoSortId; - mockService.callCluster.mockReturnValueOnce({ - took: 100, - errors: false, - items: [ - { - fakeItemValue: 'fakeItemKey', - }, - ], - }); - const successfulsingleBulkCreate = await singleBulkCreate({ - someResult: sampleSearchResult(), - ruleParams: sampleParams, - services: mockService, - logger: mockLogger, - id: sampleRuleGuid, - signalsIndex: DEFAULT_SIGNALS_INDEX, - name: 'rule-name', - createdBy: 'elastic', - updatedBy: 'elastic', - interval: '5m', - enabled: true, - tags: ['some fake tag 1', 'some fake tag 2'], - }); - expect(successfulsingleBulkCreate).toEqual(true); - }); - test('create successful bulk create with docs with no versioning', async () => { - const sampleParams = sampleRuleAlertParams(); - const sampleSearchResult = sampleDocSearchResultsNoSortIdNoVersion; - mockService.callCluster.mockReturnValueOnce({ - took: 100, - errors: false, - items: [ - { - fakeItemValue: 'fakeItemKey', - }, - ], - }); - const successfulsingleBulkCreate = await singleBulkCreate({ - someResult: sampleSearchResult(), - ruleParams: sampleParams, - services: mockService, - logger: mockLogger, - id: sampleRuleGuid, - signalsIndex: DEFAULT_SIGNALS_INDEX, - name: 'rule-name', - createdBy: 'elastic', - updatedBy: 'elastic', - interval: '5m', - enabled: true, - tags: ['some fake tag 1', 'some fake tag 2'], - }); - expect(successfulsingleBulkCreate).toEqual(true); - }); - test('create unsuccessful bulk create due to empty search results', async () => { - const sampleParams = sampleRuleAlertParams(); - const sampleSearchResult = sampleEmptyDocSearchResults; - mockService.callCluster.mockReturnValue(false); - const successfulsingleBulkCreate = await singleBulkCreate({ - someResult: sampleSearchResult, - ruleParams: sampleParams, - services: mockService, - logger: mockLogger, - id: sampleRuleGuid, - signalsIndex: DEFAULT_SIGNALS_INDEX, - name: 'rule-name', - createdBy: 'elastic', - updatedBy: 'elastic', - interval: '5m', - enabled: true, - tags: ['some fake tag 1', 'some fake tag 2'], - }); - expect(successfulsingleBulkCreate).toEqual(true); - }); - test('create successful bulk create when bulk create has errors', async () => { - const sampleParams = sampleRuleAlertParams(); - const sampleSearchResult = sampleDocSearchResultsNoSortId; - mockService.callCluster.mockReturnValue(sampleBulkCreateDuplicateResult); - const successfulsingleBulkCreate = await singleBulkCreate({ - someResult: sampleSearchResult(), - ruleParams: sampleParams, - services: mockService, - logger: mockLogger, - id: sampleRuleGuid, - signalsIndex: DEFAULT_SIGNALS_INDEX, - name: 'rule-name', - createdBy: 'elastic', - updatedBy: 'elastic', - interval: '5m', - enabled: true, - tags: ['some fake tag 1', 'some fake tag 2'], - }); - expect(mockLogger.error).toHaveBeenCalled(); - expect(successfulsingleBulkCreate).toEqual(true); - }); - }); - describe('singleSearchAfter', () => { - test('if singleSearchAfter works without a given sort id', async () => { - let searchAfterSortId; - const sampleParams = sampleRuleAlertParams(); - mockService.callCluster.mockReturnValue(sampleDocSearchResultsNoSortId); - await expect( - singleSearchAfter({ - searchAfterSortId, - ruleParams: sampleParams, - services: mockService, - logger: mockLogger, - pageSize: 1, - filter: undefined, - }) - ).rejects.toThrow('Attempted to search after with empty sort id'); - }); - test('if singleSearchAfter works with a given sort id', async () => { - const searchAfterSortId = '1234567891111'; - const sampleParams = sampleRuleAlertParams(); - mockService.callCluster.mockReturnValue(sampleDocSearchResultsWithSortId); - const searchAfterResult = await singleSearchAfter({ - searchAfterSortId, - ruleParams: sampleParams, - services: mockService, - logger: mockLogger, - pageSize: 1, - filter: undefined, - }); - expect(searchAfterResult).toEqual(sampleDocSearchResultsWithSortId); - }); - test('if singleSearchAfter throws error', async () => { - const searchAfterSortId = '1234567891111'; - const sampleParams = sampleRuleAlertParams(); - mockService.callCluster.mockImplementation(async () => { - throw Error('Fake Error'); - }); - await expect( - singleSearchAfter({ - searchAfterSortId, - ruleParams: sampleParams, - services: mockService, - logger: mockLogger, - pageSize: 1, - filter: undefined, - }) - ).rejects.toThrow('Fake Error'); - }); - }); - describe('searchAfterAndBulkCreate', () => { - test('if successful with empty search results', async () => { - const sampleParams = sampleRuleAlertParams(); - const result = await searchAfterAndBulkCreate({ - someResult: sampleEmptyDocSearchResults, - ruleParams: sampleParams, - services: mockService, - logger: mockLogger, - id: sampleRuleGuid, - signalsIndex: DEFAULT_SIGNALS_INDEX, - name: 'rule-name', - createdBy: 'elastic', - updatedBy: 'elastic', - interval: '5m', - enabled: true, - pageSize: 1, - filter: undefined, - tags: ['some fake tag 1', 'some fake tag 2'], - }); - expect(mockService.callCluster).toHaveBeenCalledTimes(0); - expect(result).toEqual(true); - }); - test('if successful iteration of while loop with maxDocs', async () => { - const sampleParams = sampleRuleAlertParams(30); - const someGuids = Array.from({ length: 13 }).map(x => uuid.v4()); - mockService.callCluster - .mockReturnValueOnce({ - took: 100, - errors: false, - items: [ - { - fakeItemValue: 'fakeItemKey', - }, - ], - }) - .mockReturnValueOnce(repeatedSearchResultsWithSortId(3, 1, someGuids.slice(0, 3))) - .mockReturnValueOnce({ - took: 100, - errors: false, - items: [ - { - fakeItemValue: 'fakeItemKey', - }, - ], - }) - .mockReturnValueOnce(repeatedSearchResultsWithSortId(3, 1, someGuids.slice(3, 6))) - .mockReturnValueOnce({ - took: 100, - errors: false, - items: [ - { - fakeItemValue: 'fakeItemKey', - }, - ], - }); - const result = await searchAfterAndBulkCreate({ - someResult: repeatedSearchResultsWithSortId(3, 1, someGuids.slice(6, 9)), - ruleParams: sampleParams, - services: mockService, - logger: mockLogger, - id: sampleRuleGuid, - signalsIndex: DEFAULT_SIGNALS_INDEX, - name: 'rule-name', - createdBy: 'elastic', - updatedBy: 'elastic', - interval: '5m', - enabled: true, - pageSize: 1, - filter: undefined, - tags: ['some fake tag 1', 'some fake tag 2'], - }); - expect(mockService.callCluster).toHaveBeenCalledTimes(5); - expect(result).toEqual(true); - }); - test('if unsuccessful first bulk create', async () => { - const someGuids = Array.from({ length: 4 }).map(x => uuid.v4()); - const sampleParams = sampleRuleAlertParams(10); - mockService.callCluster.mockReturnValue(sampleBulkCreateDuplicateResult); - const result = await searchAfterAndBulkCreate({ - someResult: repeatedSearchResultsWithSortId(4, 1, someGuids), - ruleParams: sampleParams, - services: mockService, - logger: mockLogger, - id: sampleRuleGuid, - signalsIndex: DEFAULT_SIGNALS_INDEX, - name: 'rule-name', - createdBy: 'elastic', - updatedBy: 'elastic', - interval: '5m', - enabled: true, - pageSize: 1, - filter: undefined, - tags: ['some fake tag 1', 'some fake tag 2'], - }); - expect(mockLogger.error).toHaveBeenCalled(); - expect(result).toEqual(false); - }); - test('if unsuccessful iteration of searchAfterAndBulkCreate due to empty sort ids', async () => { - const sampleParams = sampleRuleAlertParams(); - mockService.callCluster.mockReturnValueOnce({ - took: 100, - errors: false, - items: [ - { - fakeItemValue: 'fakeItemKey', - }, - ], - }); - const result = await searchAfterAndBulkCreate({ - someResult: sampleDocSearchResultsNoSortId(), - ruleParams: sampleParams, - services: mockService, - logger: mockLogger, - id: sampleRuleGuid, - signalsIndex: DEFAULT_SIGNALS_INDEX, - name: 'rule-name', - createdBy: 'elastic', - updatedBy: 'elastic', - interval: '5m', - enabled: true, - pageSize: 1, - filter: undefined, - tags: ['some fake tag 1', 'some fake tag 2'], - }); - expect(mockLogger.error).toHaveBeenCalled(); - expect(result).toEqual(false); - }); - test('if unsuccessful iteration of searchAfterAndBulkCreate due to empty sort ids and 0 total hits', async () => { - const sampleParams = sampleRuleAlertParams(); - mockService.callCluster.mockReturnValueOnce({ - took: 100, - errors: false, - items: [ - { - fakeItemValue: 'fakeItemKey', - }, - ], - }); - const result = await searchAfterAndBulkCreate({ - someResult: sampleDocSearchResultsNoSortIdNoHits(), - ruleParams: sampleParams, - services: mockService, - logger: mockLogger, - id: sampleRuleGuid, - signalsIndex: DEFAULT_SIGNALS_INDEX, - name: 'rule-name', - createdBy: 'elastic', - updatedBy: 'elastic', - interval: '5m', - enabled: true, - pageSize: 1, - filter: undefined, - tags: ['some fake tag 1', 'some fake tag 2'], - }); - expect(result).toEqual(true); - }); - test('if successful iteration of while loop with maxDocs and search after returns results with no sort ids', async () => { - const sampleParams = sampleRuleAlertParams(10); - const someGuids = Array.from({ length: 4 }).map(x => uuid.v4()); - mockService.callCluster - .mockReturnValueOnce({ - took: 100, - errors: false, - items: [ - { - fakeItemValue: 'fakeItemKey', - }, - ], - }) - .mockReturnValueOnce(sampleDocSearchResultsNoSortId()); - const result = await searchAfterAndBulkCreate({ - someResult: repeatedSearchResultsWithSortId(4, 1, someGuids), - ruleParams: sampleParams, - services: mockService, - logger: mockLogger, - id: sampleRuleGuid, - signalsIndex: DEFAULT_SIGNALS_INDEX, - name: 'rule-name', - createdBy: 'elastic', - updatedBy: 'elastic', - interval: '5m', - enabled: true, - pageSize: 1, - filter: undefined, - tags: ['some fake tag 1', 'some fake tag 2'], - }); - expect(result).toEqual(true); - }); - test('if successful iteration of while loop with maxDocs and search after returns empty results with no sort ids', async () => { - const sampleParams = sampleRuleAlertParams(10); - const someGuids = Array.from({ length: 4 }).map(x => uuid.v4()); - mockService.callCluster - .mockReturnValueOnce({ - took: 100, - errors: false, - items: [ - { - fakeItemValue: 'fakeItemKey', - }, - ], - }) - .mockReturnValueOnce(sampleEmptyDocSearchResults); - const result = await searchAfterAndBulkCreate({ - someResult: repeatedSearchResultsWithSortId(4, 1, someGuids), - ruleParams: sampleParams, - services: mockService, - logger: mockLogger, - id: sampleRuleGuid, - signalsIndex: DEFAULT_SIGNALS_INDEX, - name: 'rule-name', - createdBy: 'elastic', - updatedBy: 'elastic', - interval: '5m', - enabled: true, - pageSize: 1, - filter: undefined, - tags: ['some fake tag 1', 'some fake tag 2'], - }); - expect(result).toEqual(true); - }); - test('if returns false when singleSearchAfter throws an exception', async () => { - const sampleParams = sampleRuleAlertParams(10); - const someGuids = Array.from({ length: 4 }).map(x => uuid.v4()); - mockService.callCluster - .mockReturnValueOnce({ - took: 100, - errors: false, - items: [ - { - fakeItemValue: 'fakeItemKey', - }, - ], - }) - .mockImplementation(() => { - throw Error('Fake Error'); - }); - const result = await searchAfterAndBulkCreate({ - someResult: repeatedSearchResultsWithSortId(4, 1, someGuids), - ruleParams: sampleParams, - services: mockService, - logger: mockLogger, - id: sampleRuleGuid, - signalsIndex: DEFAULT_SIGNALS_INDEX, - name: 'rule-name', - createdBy: 'elastic', - updatedBy: 'elastic', - interval: '5m', - enabled: true, - pageSize: 1, - filter: undefined, - tags: ['some fake tag 1', 'some fake tag 2'], - }); - expect(result).toEqual(false); - }); - }); - - describe('buildEventTypeSignal', () => { - test('it returns the event appended of kind signal if it does not exist', () => { - const doc = sampleDocNoSortId(); - delete doc._source.event; - const eventType = buildEventTypeSignal(doc); - const expected: object = { kind: 'signal' }; - expect(eventType).toEqual(expected); - }); - - test('it returns the event appended of kind signal if it is an empty object', () => { - const doc = sampleDocNoSortId(); - doc._source.event = {}; - const eventType = buildEventTypeSignal(doc); - const expected: object = { kind: 'signal' }; - expect(eventType).toEqual(expected); - }); - - test('it returns the event with kind signal and other properties if they exist', () => { - const doc = sampleDocNoSortId(); - doc._source.event = { - action: 'socket_opened', - module: 'system', - dataset: 'socket', - }; - const eventType = buildEventTypeSignal(doc); - const expected: object = { - action: 'socket_opened', - module: 'system', - dataset: 'socket', - kind: 'signal', - }; - expect(eventType).toEqual(expected); - }); - }); - - describe('buildSignal', () => { - test('it builds a signal as expected without original_event if event does not exist', () => { - const doc = sampleDocNoSortId('d5e8eb51-a6a0-456d-8a15-4b79bfec3d71'); - delete doc._source.event; - const rule: Partial = sampleRule(); - const signal = buildSignal(doc, rule); - const expected: Signal = { - parent: { - id: 'd5e8eb51-a6a0-456d-8a15-4b79bfec3d71', - type: 'event', - index: 'myFakeSignalIndex', - depth: 1, - }, - original_time: 'someTimeStamp', - status: 'open', - rule: { - created_by: 'elastic', - description: 'Detecting root and admin users', - enabled: true, - false_positives: [], - from: 'now-6m', - id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', - immutable: false, - index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - interval: '5m', - risk_score: 50, - rule_id: 'rule-1', - language: 'kuery', - max_signals: 100, - name: 'Detect Root/Admin Users', - output_index: '.siem-signals', - query: 'user.name: root or user.name: admin', - references: ['http://www.example.com', 'https://ww.example.com'], - severity: 'high', - updated_by: 'elastic', - tags: ['some fake tag 1', 'some fake tag 2'], - to: 'now', - type: 'query', - }, - }; - expect(signal).toEqual(expected); - }); - - test('it builds a signal as expected with original_event if is present', () => { - const doc = sampleDocNoSortId('d5e8eb51-a6a0-456d-8a15-4b79bfec3d71'); - doc._source.event = { - action: 'socket_opened', - dataset: 'socket', - kind: 'event', - module: 'system', - }; - const rule: Partial = sampleRule(); - const signal = buildSignal(doc, rule); - const expected: Signal = { - parent: { - id: 'd5e8eb51-a6a0-456d-8a15-4b79bfec3d71', - type: 'event', - index: 'myFakeSignalIndex', - depth: 1, - }, - original_time: 'someTimeStamp', - original_event: { - action: 'socket_opened', - dataset: 'socket', - kind: 'event', - module: 'system', - }, - status: 'open', - rule: { - created_by: 'elastic', - description: 'Detecting root and admin users', - enabled: true, - false_positives: [], - from: 'now-6m', - id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', - immutable: false, - index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - interval: '5m', - risk_score: 50, - rule_id: 'rule-1', - language: 'kuery', - max_signals: 100, - name: 'Detect Root/Admin Users', - output_index: '.siem-signals', - query: 'user.name: root or user.name: admin', - references: ['http://www.example.com', 'https://ww.example.com'], - severity: 'high', - updated_by: 'elastic', - tags: ['some fake tag 1', 'some fake tag 2'], - to: 'now', - type: 'query', - }, - }; - expect(signal).toEqual(expected); - }); - }); - - describe('buildRule', () => { - test('it builds a rule as expected with filters present', () => { - const ruleParams = sampleRuleAlertParams(); - ruleParams.filters = [ - { - query: 'host.name: Rebecca', - }, - { - query: 'host.name: Evan', - }, - { - query: 'host.name: Braden', - }, - ]; - const rule = buildRule({ - ruleParams, - name: 'some-name', - id: sampleRuleGuid, - enabled: false, - createdBy: 'elastic', - updatedBy: 'elastic', - interval: 'some interval', - tags: ['some fake tag 1', 'some fake tag 2'], - }); - const expected: Partial = { - created_by: 'elastic', - description: 'Detecting root and admin users', - enabled: false, - false_positives: [], - from: 'now-6m', - id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', - immutable: false, - index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - interval: 'some interval', - language: 'kuery', - max_signals: 10000, - name: 'some-name', - output_index: '.siem-signals', - query: 'user.name: root or user.name: admin', - references: ['http://google.com'], - risk_score: 50, - rule_id: 'rule-1', - severity: 'high', - tags: ['some fake tag 1', 'some fake tag 2'], - to: 'now', - type: 'query', - updated_by: 'elastic', - filters: [ - { - query: 'host.name: Rebecca', - }, - { - query: 'host.name: Evan', - }, - { - query: 'host.name: Braden', - }, - ], - }; - expect(rule).toEqual(expected); - }); - - test('it omits a null value such as if enabled is null if is present', () => { - const ruleParams = sampleRuleAlertParams(); - ruleParams.filters = undefined; - const rule = buildRule({ - ruleParams, - name: 'some-name', - id: sampleRuleGuid, - enabled: true, - createdBy: 'elastic', - updatedBy: 'elastic', - interval: 'some interval', - tags: ['some fake tag 1', 'some fake tag 2'], - }); - const expected: Partial = { - created_by: 'elastic', - description: 'Detecting root and admin users', - enabled: true, - false_positives: [], - from: 'now-6m', - id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', - immutable: false, - index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - interval: 'some interval', - language: 'kuery', - max_signals: 10000, - name: 'some-name', - output_index: '.siem-signals', - query: 'user.name: root or user.name: admin', - references: ['http://google.com'], - risk_score: 50, - rule_id: 'rule-1', - severity: 'high', - tags: ['some fake tag 1', 'some fake tag 2'], - to: 'now', - type: 'query', - updated_by: 'elastic', - }; - expect(rule).toEqual(expected); - }); - - test('it omits a null value such as if filters is undefined if is present', () => { - const ruleParams = sampleRuleAlertParams(); - ruleParams.filters = undefined; - const rule = buildRule({ - ruleParams, - name: 'some-name', - id: sampleRuleGuid, - enabled: true, - createdBy: 'elastic', - updatedBy: 'elastic', - interval: 'some interval', - tags: ['some fake tag 1', 'some fake tag 2'], - }); - const expected: Partial = { - created_by: 'elastic', - description: 'Detecting root and admin users', - enabled: true, - false_positives: [], - from: 'now-6m', - id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', - immutable: false, - index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - interval: 'some interval', - language: 'kuery', - max_signals: 10000, - name: 'some-name', - output_index: '.siem-signals', - query: 'user.name: root or user.name: admin', - references: ['http://google.com'], - risk_score: 50, - rule_id: 'rule-1', - severity: 'high', - tags: ['some fake tag 1', 'some fake tag 2'], - to: 'now', - type: 'query', - updated_by: 'elastic', - }; - expect(rule).toEqual(expected); - }); - }); -}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.ts deleted file mode 100644 index 1787aa3a3081b..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/utils.ts +++ /dev/null @@ -1,408 +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; - * you may not use this file except in compliance with the Elastic License. - */ -import { createHash } from 'crypto'; -import { performance } from 'perf_hooks'; -import { pickBy } from 'lodash/fp'; -import { SignalHit, Signal } from '../../types'; -import { Logger } from '../../../../../../../../src/core/server'; -import { AlertServices } from '../../../../../alerting/server/types'; -import { - SignalSourceHit, - SignalSearchResponse, - BulkResponse, - RuleTypeParams, - OutputRuleAlertRest, -} from './types'; -import { buildEventsSearchQuery } from './build_events_query'; - -interface BuildRuleParams { - ruleParams: RuleTypeParams; - name: string; - id: string; - enabled: boolean; - createdBy: string; - updatedBy: string; - interval: string; - tags: string[]; -} - -export const buildRule = ({ - ruleParams, - name, - id, - enabled, - createdBy, - updatedBy, - interval, - tags, -}: BuildRuleParams): Partial => { - return pickBy((value: unknown) => value != null, { - id, - rule_id: ruleParams.ruleId, - false_positives: ruleParams.falsePositives, - saved_id: ruleParams.savedId, - meta: ruleParams.meta, - max_signals: ruleParams.maxSignals, - risk_score: ruleParams.riskScore, - output_index: ruleParams.outputIndex, - description: ruleParams.description, - from: ruleParams.from, - immutable: ruleParams.immutable, - index: ruleParams.index, - interval, - language: ruleParams.language, - name, - query: ruleParams.query, - references: ruleParams.references, - severity: ruleParams.severity, - tags, - type: ruleParams.type, - to: ruleParams.to, - enabled, - filters: ruleParams.filters, - created_by: createdBy, - updated_by: updatedBy, - threats: ruleParams.threats, - }); -}; - -export const buildSignal = (doc: SignalSourceHit, rule: Partial): Signal => { - const signal: Signal = { - parent: { - id: doc._id, - type: 'event', - index: doc._index, - depth: 1, - }, - original_time: doc._source['@timestamp'], - status: 'open', - rule, - }; - if (doc._source.event != null) { - return { ...signal, original_event: doc._source.event }; - } - return signal; -}; - -interface BuildBulkBodyParams { - doc: SignalSourceHit; - ruleParams: RuleTypeParams; - id: string; - name: string; - createdBy: string; - updatedBy: string; - interval: string; - enabled: boolean; - tags: string[]; -} - -export const buildEventTypeSignal = (doc: SignalSourceHit): object => { - if (doc._source.event != null && doc._source.event instanceof Object) { - return { ...doc._source.event, kind: 'signal' }; - } else { - return { kind: 'signal' }; - } -}; - -// format search_after result for signals index. -export const buildBulkBody = ({ - doc, - ruleParams, - id, - name, - createdBy, - updatedBy, - interval, - enabled, - tags, -}: BuildBulkBodyParams): SignalHit => { - const rule = buildRule({ - ruleParams, - id, - name, - enabled, - createdBy, - updatedBy, - interval, - tags, - }); - const signal = buildSignal(doc, rule); - const event = buildEventTypeSignal(doc); - const signalHit: SignalHit = { - ...doc._source, - '@timestamp': new Date().toISOString(), - event, - signal, - }; - return signalHit; -}; - -interface SingleBulkCreateParams { - someResult: SignalSearchResponse; - ruleParams: RuleTypeParams; - services: AlertServices; - logger: Logger; - id: string; - signalsIndex: string; - name: string; - createdBy: string; - updatedBy: string; - interval: string; - enabled: boolean; - tags: string[]; -} - -export const generateId = ( - docIndex: string, - docId: string, - version: string, - ruleId: string -): string => - createHash('sha256') - .update(docIndex.concat(docId, version, ruleId)) - .digest('hex'); - -// Bulk Index documents. -export const singleBulkCreate = async ({ - someResult, - ruleParams, - services, - logger, - id, - signalsIndex, - name, - createdBy, - updatedBy, - interval, - enabled, - tags, -}: SingleBulkCreateParams): Promise => { - if (someResult.hits.hits.length === 0) { - return true; - } - // index documents after creating an ID based on the - // source documents' originating index, and the original - // document _id. This will allow two documents from two - // different indexes with the same ID to be - // indexed, and prevents us from creating any updates - // to the documents once inserted into the signals index, - // while preventing duplicates from being added to the - // signals index if rules are re-run over the same time - // span. Also allow for versioning. - const bulkBody = someResult.hits.hits.flatMap(doc => [ - { - create: { - _index: signalsIndex, - _id: generateId( - doc._index, - doc._id, - doc._version ? doc._version.toString() : '', - ruleParams.ruleId ?? '' - ), - }, - }, - buildBulkBody({ doc, ruleParams, id, name, createdBy, updatedBy, interval, enabled, tags }), - ]); - const time1 = performance.now(); - const firstResult: BulkResponse = await services.callCluster('bulk', { - index: signalsIndex, - refresh: false, - body: bulkBody, - }); - const time2 = performance.now(); - logger.debug( - `individual bulk process time took: ${Number(time2 - time1).toFixed(2)} milliseconds` - ); - logger.debug(`took property says bulk took: ${firstResult.took} milliseconds`); - if (firstResult.errors) { - // go through the response status errors and see what - // types of errors they are, count them up, and log them. - const errorCountMap = firstResult.items.reduce((acc: { [key: string]: number }, item) => { - if (item.create.error) { - const responseStatusKey = item.create.status.toString(); - acc[responseStatusKey] = acc[responseStatusKey] ? acc[responseStatusKey] + 1 : 1; - } - return acc; - }, {}); - /* - the logging output below should look like - {'409': 55} - which is read as "there were 55 counts of 409 errors returned from bulk create" - */ - logger.error( - `[-] bulkResponse had errors with response statuses:counts of...\n${JSON.stringify( - errorCountMap, - null, - 2 - )}` - ); - } - return true; -}; - -interface SingleSearchAfterParams { - searchAfterSortId: string | undefined; - ruleParams: RuleTypeParams; - services: AlertServices; - logger: Logger; - pageSize: number; - filter: unknown; -} - -// utilize search_after for paging results into bulk. -export const singleSearchAfter = async ({ - searchAfterSortId, - ruleParams, - services, - filter, - logger, - pageSize, -}: SingleSearchAfterParams): Promise => { - if (searchAfterSortId == null) { - throw Error('Attempted to search after with empty sort id'); - } - try { - const searchAfterQuery = buildEventsSearchQuery({ - index: ruleParams.index, - from: ruleParams.from, - to: ruleParams.to, - filter, - size: pageSize, - searchAfterSortId, - }); - const nextSearchAfterResult: SignalSearchResponse = await services.callCluster( - 'search', - searchAfterQuery - ); - return nextSearchAfterResult; - } catch (exc) { - logger.error(`[-] nextSearchAfter threw an error ${exc}`); - throw exc; - } -}; - -interface SearchAfterAndBulkCreateParams { - someResult: SignalSearchResponse; - ruleParams: RuleTypeParams; - services: AlertServices; - logger: Logger; - id: string; - signalsIndex: string; - name: string; - createdBy: string; - updatedBy: string; - interval: string; - enabled: boolean; - pageSize: number; - filter: unknown; - tags: string[]; -} - -// search_after through documents and re-index using bulk endpoint. -export const searchAfterAndBulkCreate = async ({ - someResult, - ruleParams, - services, - logger, - id, - signalsIndex, - filter, - name, - createdBy, - updatedBy, - interval, - enabled, - pageSize, - tags, -}: SearchAfterAndBulkCreateParams): Promise => { - if (someResult.hits.hits.length === 0) { - return true; - } - - logger.debug('[+] starting bulk insertion'); - await singleBulkCreate({ - someResult, - ruleParams, - services, - logger, - id, - signalsIndex, - name, - createdBy, - updatedBy, - interval, - enabled, - tags, - }); - const totalHits = - typeof someResult.hits.total === 'number' ? someResult.hits.total : someResult.hits.total.value; - // maxTotalHitsSize represents the total number of docs to - // query for, no matter the size of each individual page of search results. - // If the total number of hits for the overall search result is greater than - // maxSignals, default to requesting a total of maxSignals, otherwise use the - // totalHits in the response from the searchAfter query. - const maxTotalHitsSize = totalHits >= ruleParams.maxSignals ? ruleParams.maxSignals : totalHits; - - // number of docs in the current search result - let hitsSize = someResult.hits.hits.length; - logger.debug(`first size: ${hitsSize}`); - let sortIds = someResult.hits.hits[0].sort; - if (sortIds == null && totalHits > 0) { - logger.error('sortIds was empty on first search but expected more'); - return false; - } else if (sortIds == null && totalHits === 0) { - return true; - } - let sortId; - if (sortIds != null) { - sortId = sortIds[0]; - } - while (hitsSize < maxTotalHitsSize && hitsSize !== 0) { - try { - logger.debug(`sortIds: ${sortIds}`); - const searchAfterResult: SignalSearchResponse = await singleSearchAfter({ - searchAfterSortId: sortId, - ruleParams, - services, - logger, - filter, - pageSize, // maximum number of docs to receive per search result. - }); - if (searchAfterResult.hits.hits.length === 0) { - return true; - } - hitsSize += searchAfterResult.hits.hits.length; - logger.debug(`size adjusted: ${hitsSize}`); - sortIds = searchAfterResult.hits.hits[0].sort; - if (sortIds == null) { - logger.debug('sortIds was empty on search'); - return true; // no more search results - } - sortId = sortIds[0]; - logger.debug('next bulk index'); - await singleBulkCreate({ - someResult: searchAfterResult, - ruleParams, - services, - logger, - id, - signalsIndex, - name, - createdBy, - updatedBy, - interval, - enabled, - tags, - }); - logger.debug('finished next bulk index'); - } catch (exc) { - logger.error(`[-] search_after and bulk threw an error ${exc}`); - return false; - } - } - logger.debug(`[+] completed bulk index of ${maxTotalHitsSize}`); - return true; -}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts index cd8b716221b9b..978434859ef95 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts @@ -6,11 +6,13 @@ import { ServerInjectOptions } from 'hapi'; import { ActionResult } from '../../../../../../actions/server/types'; -import { RuleAlertParamsRest, RuleAlertType, SignalsRestParams } from '../../alerts/types'; +import { SignalsRestParams } from '../../signals/types'; import { DETECTION_ENGINE_RULES_URL, DETECTION_ENGINE_SIGNALS_STATUS_URL, } from '../../../../../common/constants'; +import { RuleAlertType } from '../../rules/types'; +import { RuleAlertParamsRest } from '../../types'; // The Omit of filter is because of a Hapi Server Typing issue that I am unclear // where it comes from. I would hope to remove the "filter" as an omit at some point diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts similarity index 96% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_rules_route.test.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts index b271af2db1e7d..094449a5f61ac 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts @@ -9,7 +9,7 @@ import { createMockServerWithoutActionClientDecoration, createMockServerWithoutAlertClientDecoration, createMockServerWithoutActionOrAlertClientDecoration, -} from './__mocks__/_mock_server'; +} from '../__mocks__/_mock_server'; import { createRulesRoute } from './create_rules_route'; import { ServerInjectOptions } from 'hapi'; import { @@ -18,8 +18,8 @@ import { createActionResult, getCreateRequest, typicalPayload, -} from './__mocks__/request_responses'; -import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; +} from '../__mocks__/request_responses'; +import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; describe('create_rules', () => { let { server, alertsClient, actionsClient, elasticsearch } = createMockServer(); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts similarity index 85% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_rules_route.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts index a137d54250189..0dc213e9e2173 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts @@ -8,14 +8,15 @@ import Hapi from 'hapi'; import { isFunction } from 'lodash/fp'; import Boom from 'boom'; import uuid from 'uuid'; -import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; -import { createRules } from '../alerts/create_rules'; -import { RulesRequest } from '../alerts/types'; -import { createRulesSchema } from './schemas'; -import { ServerFacade } from '../../../types'; -import { readRules } from '../alerts/read_rules'; -import { transformOrError, transformError, getIndex, callWithRequestFactory } from './utils'; -import { getIndexExists } from '../index/get_index_exists'; +import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; +import { createRules } from '../../rules/create_rules'; +import { RulesRequest } from '../../rules/types'; +import { createRulesSchema } from '../schemas/create_rules_schema'; +import { ServerFacade } from '../../../../types'; +import { readRules } from '../../rules/read_rules'; +import { transformOrError } from './utils'; +import { getIndexExists } from '../../index/get_index_exists'; +import { callWithRequestFactory, getIndex, transformError } from '../utils'; export const createCreateRulesRoute = (server: ServerFacade): Hapi.ServerRoute => { return { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.test.ts similarity index 96% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_rules_route.test.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.test.ts index 0808051964dc1..cacafcf741e6a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.test.ts @@ -9,7 +9,7 @@ import { createMockServerWithoutActionClientDecoration, createMockServerWithoutAlertClientDecoration, createMockServerWithoutActionOrAlertClientDecoration, -} from './__mocks__/_mock_server'; +} from '../__mocks__/_mock_server'; import { deleteRulesRoute } from './delete_rules_route'; import { ServerInjectOptions } from 'hapi'; @@ -19,8 +19,8 @@ import { getDeleteRequest, getFindResultWithSingleHit, getDeleteRequestById, -} from './__mocks__/request_responses'; -import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; +} from '../__mocks__/request_responses'; +import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; describe('delete_rules', () => { let { server, alertsClient } = createMockServer(); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.ts similarity index 78% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_rules_route.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.ts index fe8b139f11c01..c2b2e2fdbbaef 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.ts @@ -7,12 +7,13 @@ import Hapi from 'hapi'; import { isFunction } from 'lodash/fp'; -import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; -import { deleteRules } from '../alerts/delete_rules'; -import { ServerFacade } from '../../../types'; -import { queryRulesSchema } from './schemas'; -import { QueryRequest } from '../alerts/types'; -import { getIdError, transformOrError, transformError } from './utils'; +import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; +import { deleteRules } from '../../rules/delete_rules'; +import { ServerFacade } from '../../../../types'; +import { queryRulesSchema } from '../schemas/query_rules_schema'; +import { getIdError, transformOrError } from './utils'; +import { transformError } from '../utils'; +import { QueryRequest } from './types'; export const createDeleteRulesRoute: Hapi.ServerRoute = { method: 'DELETE', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.test.ts similarity index 94% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_rules_route.test.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.test.ts index dae40f05155dc..38937c13d302c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.test.ts @@ -9,12 +9,12 @@ import { createMockServerWithoutActionClientDecoration, createMockServerWithoutAlertClientDecoration, createMockServerWithoutActionOrAlertClientDecoration, -} from './__mocks__/_mock_server'; +} from '../__mocks__/_mock_server'; import { findRulesRoute } from './find_rules_route'; import { ServerInjectOptions } from 'hapi'; -import { getFindResult, getResult, getFindRequest } from './__mocks__/request_responses'; -import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; +import { getFindResult, getResult, getFindRequest } from '../__mocks__/request_responses'; +import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; describe('find_rules', () => { let { server, alertsClient, actionsClient } = createMockServer(); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.ts similarity index 78% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_rules_route.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.ts index 137dd9352699e..6e89ddb19017d 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.ts @@ -6,12 +6,13 @@ import Hapi from 'hapi'; import { isFunction } from 'lodash/fp'; -import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; -import { findRules } from '../alerts/find_rules'; -import { FindRulesRequest } from '../alerts/types'; -import { findRulesSchema } from './schemas'; -import { ServerFacade } from '../../../types'; -import { transformFindAlertsOrError, transformError } from './utils'; +import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; +import { findRules } from '../../rules/find_rules'; +import { FindRulesRequest } from '../../rules/types'; +import { findRulesSchema } from '../schemas/find_rules_schema'; +import { ServerFacade } from '../../../../types'; +import { transformFindAlertsOrError } from './utils'; +import { transformError } from '../utils'; export const createFindRulesRoute: Hapi.ServerRoute = { method: 'GET', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.test.ts similarity index 94% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_rules_route.test.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.test.ts index 47ecf62f41be9..0d77583573c13 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.test.ts @@ -9,7 +9,7 @@ import { createMockServerWithoutActionClientDecoration, createMockServerWithoutAlertClientDecoration, createMockServerWithoutActionOrAlertClientDecoration, -} from './__mocks__/_mock_server'; +} from '../__mocks__/_mock_server'; import { readRulesRoute } from './read_rules_route'; import { ServerInjectOptions } from 'hapi'; @@ -18,8 +18,8 @@ import { getResult, getReadRequest, getFindResultWithSingleHit, -} from './__mocks__/request_responses'; -import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; +} from '../__mocks__/request_responses'; +import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; describe('read_signals', () => { let { server, alertsClient } = createMockServer(); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.ts similarity index 78% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_rules_route.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.ts index a7bda40fdc523..a842e68b6b7fe 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.ts @@ -6,13 +6,14 @@ import Hapi from 'hapi'; import { isFunction } from 'lodash/fp'; -import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; -import { getIdError, transformOrError, transformError } from './utils'; +import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; +import { getIdError, transformOrError } from './utils'; +import { transformError } from '../utils'; -import { readRules } from '../alerts/read_rules'; -import { ServerFacade } from '../../../types'; -import { queryRulesSchema } from './schemas'; -import { QueryRequest } from '../alerts/types'; +import { readRules } from '../../rules/read_rules'; +import { ServerFacade } from '../../../../types'; +import { queryRulesSchema } from '../schemas/query_rules_schema'; +import { QueryRequest } from './types'; export const createReadRulesRoute: Hapi.ServerRoute = { method: 'GET', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/types.ts new file mode 100644 index 0000000000000..f6878c9edc9b8 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/types.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { RequestFacade } from '../../../../types'; + +export type QueryRequest = Omit & { + query: { id: string | undefined; rule_id: string | undefined }; +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.test.ts similarity index 97% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_rules_route.test.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.test.ts index dfa1275a6b26b..3cf5c07655d92 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.test.ts @@ -9,7 +9,7 @@ import { createMockServerWithoutActionClientDecoration, createMockServerWithoutAlertClientDecoration, createMockServerWithoutActionOrAlertClientDecoration, -} from './__mocks__/_mock_server'; +} from '../__mocks__/_mock_server'; import { updateRulesRoute } from './update_rules_route'; import { ServerInjectOptions } from 'hapi'; @@ -20,8 +20,8 @@ import { getUpdateRequest, typicalPayload, getFindResultWithSingleHit, -} from './__mocks__/request_responses'; -import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; +} from '../__mocks__/request_responses'; +import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; describe('update_rules', () => { let { server, alertsClient, actionsClient } = createMockServer(); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts similarity index 84% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_rules_route.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts index 943c41fd6dea6..2e7b48afbb5d9 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts @@ -6,12 +6,13 @@ import Hapi from 'hapi'; import { isFunction } from 'lodash/fp'; -import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants'; -import { updateRules } from '../alerts/update_rules'; -import { UpdateRulesRequest } from '../alerts/types'; -import { updateRulesSchema } from './schemas'; -import { ServerFacade } from '../../../types'; -import { getIdError, transformOrError, transformError } from './utils'; +import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; +import { updateRules } from '../../rules/update_rules'; +import { UpdateRulesRequest } from '../../rules/types'; +import { updateRulesSchema } from '../schemas/update_rules_schema'; +import { ServerFacade } from '../../../../types'; +import { getIdError, transformOrError } from './utils'; +import { transformError } from '../utils'; export const createUpdateRulesRoute: Hapi.ServerRoute = { method: 'PUT', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts new file mode 100644 index 0000000000000..d4e129f543ccf --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts @@ -0,0 +1,496 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Boom from 'boom'; + +import { + transformAlertToRule, + getIdError, + transformFindAlertsOrError, + transformOrError, +} from './utils'; +import { getResult } from '../__mocks__/request_responses'; + +describe('utils', () => { + describe('transformAlertToRule', () => { + test('should work with a full data set', () => { + const fullRule = getResult(); + const rule = transformAlertToRule(fullRule); + expect(rule).toEqual({ + created_by: 'elastic', + description: 'Detecting root and admin users', + enabled: true, + false_positives: [], + from: 'now-6m', + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + immutable: false, + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + interval: '5m', + risk_score: 50, + rule_id: 'rule-1', + language: 'kuery', + max_signals: 100, + name: 'Detect Root/Admin Users', + output_index: '.siem-signals', + query: 'user.name: root or user.name: admin', + references: ['http://www.example.com', 'https://ww.example.com'], + severity: 'high', + updated_by: 'elastic', + tags: [], + threats: [ + { + framework: 'MITRE ATT&CK', + tactic: { + id: 'TA0040', + name: 'impact', + reference: 'https://attack.mitre.org/tactics/TA0040/', + }, + techniques: [ + { + id: 'T1499', + name: 'endpoint denial of service', + reference: 'https://attack.mitre.org/techniques/T1499/', + }, + ], + }, + ], + to: 'now', + type: 'query', + }); + }); + + test('should work with a partial data set missing data', () => { + const fullRule = getResult(); + const { from, language, ...omitData } = transformAlertToRule(fullRule); + expect(omitData).toEqual({ + created_by: 'elastic', + description: 'Detecting root and admin users', + enabled: true, + false_positives: [], + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + immutable: false, + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + output_index: '.siem-signals', + interval: '5m', + risk_score: 50, + rule_id: 'rule-1', + max_signals: 100, + name: 'Detect Root/Admin Users', + query: 'user.name: root or user.name: admin', + references: ['http://www.example.com', 'https://ww.example.com'], + severity: 'high', + updated_by: 'elastic', + tags: [], + threats: [ + { + framework: 'MITRE ATT&CK', + tactic: { + id: 'TA0040', + name: 'impact', + reference: 'https://attack.mitre.org/tactics/TA0040/', + }, + techniques: [ + { + id: 'T1499', + name: 'endpoint denial of service', + reference: 'https://attack.mitre.org/techniques/T1499/', + }, + ], + }, + ], + to: 'now', + type: 'query', + }); + }); + + test('should omit query if query is null', () => { + const fullRule = getResult(); + fullRule.params.query = null; + const rule = transformAlertToRule(fullRule); + expect(rule).toEqual({ + created_by: 'elastic', + description: 'Detecting root and admin users', + enabled: true, + false_positives: [], + from: 'now-6m', + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + immutable: false, + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + output_index: '.siem-signals', + interval: '5m', + risk_score: 50, + rule_id: 'rule-1', + language: 'kuery', + max_signals: 100, + name: 'Detect Root/Admin Users', + references: ['http://www.example.com', 'https://ww.example.com'], + severity: 'high', + updated_by: 'elastic', + tags: [], + threats: [ + { + framework: 'MITRE ATT&CK', + tactic: { + id: 'TA0040', + name: 'impact', + reference: 'https://attack.mitre.org/tactics/TA0040/', + }, + techniques: [ + { + id: 'T1499', + name: 'endpoint denial of service', + reference: 'https://attack.mitre.org/techniques/T1499/', + }, + ], + }, + ], + to: 'now', + type: 'query', + }); + }); + + test('should omit query if query is undefined', () => { + const fullRule = getResult(); + fullRule.params.query = undefined; + const rule = transformAlertToRule(fullRule); + expect(rule).toEqual({ + created_by: 'elastic', + description: 'Detecting root and admin users', + enabled: true, + false_positives: [], + from: 'now-6m', + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + immutable: false, + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + output_index: '.siem-signals', + interval: '5m', + rule_id: 'rule-1', + risk_score: 50, + language: 'kuery', + max_signals: 100, + name: 'Detect Root/Admin Users', + references: ['http://www.example.com', 'https://ww.example.com'], + severity: 'high', + updated_by: 'elastic', + tags: [], + threats: [ + { + framework: 'MITRE ATT&CK', + tactic: { + id: 'TA0040', + name: 'impact', + reference: 'https://attack.mitre.org/tactics/TA0040/', + }, + techniques: [ + { + id: 'T1499', + name: 'endpoint denial of service', + reference: 'https://attack.mitre.org/techniques/T1499/', + }, + ], + }, + ], + to: 'now', + type: 'query', + }); + }); + + test('should omit a mix of undefined, null, and missing fields', () => { + const fullRule = getResult(); + fullRule.params.query = undefined; + fullRule.params.language = null; + const { from, enabled, ...omitData } = transformAlertToRule(fullRule); + expect(omitData).toEqual({ + created_by: 'elastic', + description: 'Detecting root and admin users', + false_positives: [], + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + immutable: false, + output_index: '.siem-signals', + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + interval: '5m', + rule_id: 'rule-1', + risk_score: 50, + max_signals: 100, + name: 'Detect Root/Admin Users', + references: ['http://www.example.com', 'https://ww.example.com'], + severity: 'high', + updated_by: 'elastic', + tags: [], + threats: [ + { + framework: 'MITRE ATT&CK', + tactic: { + id: 'TA0040', + name: 'impact', + reference: 'https://attack.mitre.org/tactics/TA0040/', + }, + techniques: [ + { + id: 'T1499', + name: 'endpoint denial of service', + reference: 'https://attack.mitre.org/techniques/T1499/', + }, + ], + }, + ], + to: 'now', + type: 'query', + }); + }); + + test('should return enabled is equal to false', () => { + const fullRule = getResult(); + fullRule.enabled = false; + const ruleWithEnabledFalse = transformAlertToRule(fullRule); + expect(ruleWithEnabledFalse).toEqual({ + created_by: 'elastic', + description: 'Detecting root and admin users', + enabled: false, + from: 'now-6m', + false_positives: [], + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + immutable: false, + output_index: '.siem-signals', + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + interval: '5m', + language: 'kuery', + risk_score: 50, + rule_id: 'rule-1', + max_signals: 100, + name: 'Detect Root/Admin Users', + query: 'user.name: root or user.name: admin', + references: ['http://www.example.com', 'https://ww.example.com'], + severity: 'high', + updated_by: 'elastic', + tags: [], + threats: [ + { + framework: 'MITRE ATT&CK', + tactic: { + id: 'TA0040', + name: 'impact', + reference: 'https://attack.mitre.org/tactics/TA0040/', + }, + techniques: [ + { + id: 'T1499', + name: 'endpoint denial of service', + reference: 'https://attack.mitre.org/techniques/T1499/', + }, + ], + }, + ], + to: 'now', + type: 'query', + }); + }); + + test('should return immutable is equal to false', () => { + const fullRule = getResult(); + fullRule.params.immutable = false; + const ruleWithEnabledFalse = transformAlertToRule(fullRule); + expect(ruleWithEnabledFalse).toEqual({ + created_by: 'elastic', + description: 'Detecting root and admin users', + enabled: true, + from: 'now-6m', + false_positives: [], + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + immutable: false, + output_index: '.siem-signals', + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + interval: '5m', + language: 'kuery', + risk_score: 50, + rule_id: 'rule-1', + max_signals: 100, + name: 'Detect Root/Admin Users', + query: 'user.name: root or user.name: admin', + references: ['http://www.example.com', 'https://ww.example.com'], + severity: 'high', + updated_by: 'elastic', + tags: [], + threats: [ + { + framework: 'MITRE ATT&CK', + tactic: { + id: 'TA0040', + name: 'impact', + reference: 'https://attack.mitre.org/tactics/TA0040/', + }, + techniques: [ + { + id: 'T1499', + name: 'endpoint denial of service', + reference: 'https://attack.mitre.org/techniques/T1499/', + }, + ], + }, + ], + to: 'now', + type: 'query', + }); + }); + }); + + describe('getIdError', () => { + test('outputs message about id not being found if only id is defined and ruleId is undefined', () => { + const boom = getIdError({ id: '123', ruleId: undefined }); + expect(boom.message).toEqual('id: "123" not found'); + }); + + test('outputs message about id not being found if only id is defined and ruleId is null', () => { + const boom = getIdError({ id: '123', ruleId: null }); + expect(boom.message).toEqual('id: "123" not found'); + }); + + test('outputs message about ruleId not being found if only ruleId is defined and id is undefined', () => { + const boom = getIdError({ id: undefined, ruleId: 'rule-id-123' }); + expect(boom.message).toEqual('rule_id: "rule-id-123" not found'); + }); + + test('outputs message about ruleId not being found if only ruleId is defined and id is null', () => { + const boom = getIdError({ id: null, ruleId: 'rule-id-123' }); + expect(boom.message).toEqual('rule_id: "rule-id-123" not found'); + }); + + test('outputs message about both being not defined when both are undefined', () => { + const boom = getIdError({ id: undefined, ruleId: undefined }); + expect(boom.message).toEqual('id or rule_id should have been defined'); + }); + + test('outputs message about both being not defined when both are null', () => { + const boom = getIdError({ id: null, ruleId: null }); + expect(boom.message).toEqual('id or rule_id should have been defined'); + }); + + test('outputs message about both being not defined when id is null and ruleId is undefined', () => { + const boom = getIdError({ id: null, ruleId: undefined }); + expect(boom.message).toEqual('id or rule_id should have been defined'); + }); + + test('outputs message about both being not defined when id is undefined and ruleId is null', () => { + const boom = getIdError({ id: undefined, ruleId: null }); + expect(boom.message).toEqual('id or rule_id should have been defined'); + }); + }); + + describe('transformFindAlertsOrError', () => { + test('outputs empty data set when data set is empty correct', () => { + const output = transformFindAlertsOrError({ data: [] }); + expect(output).toEqual({ data: [] }); + }); + + test('outputs 200 if the data is of type siem alert', () => { + const output = transformFindAlertsOrError({ + data: [getResult()], + }); + expect(output).toEqual({ + data: [ + { + created_by: 'elastic', + description: 'Detecting root and admin users', + enabled: true, + false_positives: [], + from: 'now-6m', + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + immutable: false, + output_index: '.siem-signals', + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + interval: '5m', + risk_score: 50, + rule_id: 'rule-1', + language: 'kuery', + max_signals: 100, + name: 'Detect Root/Admin Users', + query: 'user.name: root or user.name: admin', + references: ['http://www.example.com', 'https://ww.example.com'], + severity: 'high', + updated_by: 'elastic', + tags: [], + to: 'now', + type: 'query', + threats: [ + { + framework: 'MITRE ATT&CK', + tactic: { + id: 'TA0040', + name: 'impact', + reference: 'https://attack.mitre.org/tactics/TA0040/', + }, + techniques: [ + { + id: 'T1499', + name: 'endpoint denial of service', + reference: 'https://attack.mitre.org/techniques/T1499/', + }, + ], + }, + ], + }, + ], + }); + }); + + test('returns 500 if the data is not of type siem alert', () => { + const output = transformFindAlertsOrError({ data: [{ random: 1 }] }); + expect((output as Boom).message).toEqual('Internal error transforming'); + }); + }); + + describe('transformOrError', () => { + test('outputs 200 if the data is of type siem alert', () => { + const output = transformOrError(getResult()); + expect(output).toEqual({ + created_by: 'elastic', + description: 'Detecting root and admin users', + enabled: true, + false_positives: [], + from: 'now-6m', + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + immutable: false, + output_index: '.siem-signals', + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + interval: '5m', + rule_id: 'rule-1', + risk_score: 50, + language: 'kuery', + max_signals: 100, + name: 'Detect Root/Admin Users', + query: 'user.name: root or user.name: admin', + references: ['http://www.example.com', 'https://ww.example.com'], + severity: 'high', + updated_by: 'elastic', + tags: [], + to: 'now', + type: 'query', + threats: [ + { + framework: 'MITRE ATT&CK', + tactic: { + id: 'TA0040', + name: 'impact', + reference: 'https://attack.mitre.org/tactics/TA0040/', + }, + techniques: [ + { + id: 'T1499', + name: 'endpoint denial of service', + reference: 'https://attack.mitre.org/techniques/T1499/', + }, + ], + }, + ], + }); + }); + + test('returns 500 if the data is not of type siem alert', () => { + const output = transformOrError({ data: [{ random: 1 }] }); + expect((output as Boom).message).toEqual('Internal error transforming'); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts new file mode 100644 index 0000000000000..c9ae3abdfdc6b --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Boom from 'boom'; +import { pickBy } from 'lodash/fp'; +import { RuleAlertType, isAlertType, isAlertTypes } from '../../rules/types'; +import { OutputRuleAlertRest } from '../../types'; + +export const getIdError = ({ + id, + ruleId, +}: { + id: string | undefined | null; + ruleId: string | undefined | null; +}) => { + if (id != null) { + return new Boom(`id: "${id}" not found`, { statusCode: 404 }); + } else if (ruleId != null) { + return new Boom(`rule_id: "${ruleId}" not found`, { statusCode: 404 }); + } else { + return new Boom(`id or rule_id should have been defined`, { statusCode: 404 }); + } +}; + +// Transforms the data but will remove any null or undefined it encounters and not include +// those on the export +export const transformAlertToRule = (alert: RuleAlertType): Partial => { + return pickBy((value: unknown) => value != null, { + created_by: alert.createdBy, + description: alert.params.description, + enabled: alert.enabled, + false_positives: alert.params.falsePositives, + filters: alert.params.filters, + from: alert.params.from, + id: alert.id, + immutable: alert.params.immutable, + index: alert.params.index, + interval: alert.interval, + rule_id: alert.params.ruleId, + language: alert.params.language, + output_index: alert.params.outputIndex, + max_signals: alert.params.maxSignals, + risk_score: alert.params.riskScore, + name: alert.name, + query: alert.params.query, + references: alert.params.references, + saved_id: alert.params.savedId, + meta: alert.params.meta, + severity: alert.params.severity, + updated_by: alert.updatedBy, + tags: alert.tags, + to: alert.params.to, + type: alert.params.type, + threats: alert.params.threats, + }); +}; + +export const transformFindAlertsOrError = (findResults: { data: unknown[] }): unknown | Boom => { + if (isAlertTypes(findResults.data)) { + findResults.data = findResults.data.map(alert => transformAlertToRule(alert)); + return findResults; + } else { + return new Boom('Internal error transforming', { statusCode: 500 }); + } +}; + +export const transformOrError = (alert: unknown): Partial | Boom => { + if (isAlertType(alert)) { + return transformAlertToRule(alert); + } else { + return new Boom('Internal error transforming', { statusCode: 500 }); + } +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.test.ts deleted file mode 100644 index f5147bc5a8f8b..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.test.ts +++ /dev/null @@ -1,2133 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { - createRulesSchema, - updateRulesSchema, - findRulesSchema, - queryRulesSchema, - setSignalsStatusSchema, -} from './schemas'; -import { - RuleAlertParamsRest, - FindParamsRest, - UpdateRuleAlertParamsRest, - ThreatParams, - SignalsRestParams, -} from '../alerts/types'; - -describe('schemas', () => { - describe('create rules schema', () => { - test('empty objects do not validate', () => { - expect(createRulesSchema.validate>({}).error).toBeTruthy(); - }); - - test('made up values do not validate', () => { - expect( - createRulesSchema.validate>({ - madeUp: 'hi', - }).error - ).toBeTruthy(); - }); - - test('[rule_id] does not validate', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - }).error - ).toBeTruthy(); - }); - - test('[rule_id, description] does not validate', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - }).error - ).toBeTruthy(); - }); - - test('[rule_id, description, from] does not validate', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - }).error - ).toBeTruthy(); - }); - - test('[rule_id, description, from, to] does not validate', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - }).error - ).toBeTruthy(); - }); - - test('[rule_id, description, from, to, name] does not validate', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - name: 'some-name', - }).error - ).toBeTruthy(); - }); - - test('[rule_id, description, from, to, name, severity] does not validate', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - name: 'some-name', - severity: 'severity', - }).error - ).toBeTruthy(); - }); - - test('[rule_id, description, from, to, name, severity, type] does not validate', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - name: 'some-name', - severity: 'severity', - type: 'query', - }).error - ).toBeTruthy(); - }); - - test('[rule_id, description, from, to, name, severity, type, interval] does not validate', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - name: 'some-name', - severity: 'severity', - interval: '5m', - type: 'query', - }).error - ).toBeTruthy(); - }); - - test('[rule_id, description, from, to, name, severity, type, interval, index] does not validate', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - name: 'some-name', - severity: 'severity', - type: 'query', - interval: '5m', - index: ['index-1'], - }).error - ).toBeTruthy(); - }); - - test('[rule_id, description, from, to, name, severity, type, query, index, interval] does validate', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - name: 'some-name', - severity: 'severity', - type: 'query', - query: 'some query', - index: ['index-1'], - interval: '5m', - }).error - ).toBeFalsy(); - }); - - test('[rule_id, description, from, to, index, name, severity, interval, type, query, language] does not validate', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'severity', - interval: '5m', - type: 'query', - query: 'some query', - language: 'kuery', - }).error - ).toBeTruthy(); - }); - - test('[rule_id, description, from, to, index, name, severity, interval, type, query, language, risk_score] does validate', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'severity', - interval: '5m', - type: 'query', - query: 'some query', - language: 'kuery', - }).error - ).toBeFalsy(); - }); - - test('[rule_id, description, from, to, index, name, severity, interval, type, query, language, risk_score, output_index] does validate', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'severity', - interval: '5m', - type: 'query', - query: 'some query', - language: 'kuery', - }).error - ).toBeFalsy(); - }); - - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score] does validate', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'severity', - interval: '5m', - type: 'query', - risk_score: 50, - }).error - ).toBeFalsy(); - }); - - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, output_index] does validate', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'severity', - interval: '5m', - type: 'query', - }).error - ).toBeFalsy(); - }); - test('You can send in an empty array to threats', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'severity', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - threats: [], - }).error - ).toBeFalsy(); - }); - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, output_index, threats] does validate', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'severity', - interval: '5m', - type: 'query', - threats: [ - { - framework: 'someFramework', - tactic: { - id: 'fakeId', - name: 'fakeName', - reference: 'fakeRef', - }, - techniques: [ - { - id: 'techniqueId', - name: 'techniqueName', - reference: 'techniqueRef', - }, - ], - }, - ], - }).error - ).toBeFalsy(); - }); - - test('allows references to be sent as valid', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'severity', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - }).error - ).toBeFalsy(); - }); - - test('defaults references to an array', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'severity', - interval: '5m', - type: 'query', - query: 'some-query', - language: 'kuery', - }).value.references - ).toEqual([]); - }); - - test('references cannot be numbers', () => { - expect( - createRulesSchema.validate< - Partial> & { references: number[] } - >({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'severity', - interval: '5m', - type: 'query', - query: 'some-query', - language: 'kuery', - references: [5], - }).error - ).toBeTruthy(); - }); - - test('indexes cannot be numbers', () => { - expect( - createRulesSchema.validate< - Partial> & { index: number[] } - >({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: [5], - name: 'some-name', - severity: 'severity', - interval: '5m', - type: 'query', - query: 'some-query', - language: 'kuery', - }).error - ).toBeTruthy(); - }); - - test('defaults interval to 5 min', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'severity', - type: 'query', - }).value.interval - ).toEqual('5m'); - }); - - test('defaults max signals to 100', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'severity', - interval: '5m', - type: 'query', - }).value.max_signals - ).toEqual(100); - }); - - test('saved_id is required when type is saved_query and will not validate without out', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'severity', - interval: '5m', - type: 'saved_query', - }).error - ).toBeTruthy(); - }); - - test('saved_id is required when type is saved_query and validates with it', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - risk_score: 50, - output_index: '.siem-signals', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'severity', - interval: '5m', - type: 'saved_query', - saved_id: 'some id', - }).error - ).toBeFalsy(); - }); - - test('saved_query type can have filters with it', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'severity', - interval: '5m', - type: 'saved_query', - saved_id: 'some id', - filters: [], - }).error - ).toBeFalsy(); - }); - - test('filters cannot be a string', () => { - expect( - createRulesSchema.validate< - Partial & { filters: string }> - >({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'severity', - interval: '5m', - type: 'saved_query', - saved_id: 'some id', - filters: 'some string', - }).error - ).toBeTruthy(); - }); - - test('language validates with kuery', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'severity', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - }).error - ).toBeFalsy(); - }); - - test('language validates with lucene', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - risk_score: 50, - output_index: '.siem-signals', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'severity', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'lucene', - }).error - ).toBeFalsy(); - }); - - test('language does not validate with something made up', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'severity', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'something-made-up', - }).error - ).toBeTruthy(); - }); - - test('max_signals cannot be negative', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'severity', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: -1, - }).error - ).toBeTruthy(); - }); - - test('max_signals cannot be zero', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'severity', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 0, - }).error - ).toBeTruthy(); - }); - - test('max_signals can be 1', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'severity', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - }).error - ).toBeFalsy(); - }); - - test('You can optionally send in an array of tags', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'severity', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - tags: ['tag_1', 'tag_2'], - }).error - ).toBeFalsy(); - }); - - test('You cannot send in an array of tags that are numbers', () => { - expect( - createRulesSchema.validate> & { tags: number[] }>( - { - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'severity', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - tags: [0, 1, 2], - } - ).error - ).toBeTruthy(); - }); - - test('You cannot send in an array of threats that are missing "framework"', () => { - expect( - createRulesSchema.validate< - Partial> & { - threats: Array>>; - } - >({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'severity', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - threats: [ - { - tactic: { - id: 'fakeId', - name: 'fakeName', - reference: 'fakeRef', - }, - techniques: [ - { - id: 'techniqueId', - name: 'techniqueName', - reference: 'techniqueRef', - }, - ], - }, - ], - }).error - ).toBeTruthy(); - }); - test('You cannot send in an array of threats that are missing "tactic"', () => { - expect( - createRulesSchema.validate< - Partial> & { - threats: Array>>; - } - >({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'severity', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - threats: [ - { - framework: 'fake', - techniques: [ - { - id: 'techniqueId', - name: 'techniqueName', - reference: 'techniqueRef', - }, - ], - }, - ], - }).error - ).toBeTruthy(); - }); - test('You cannot send in an array of threats that are missing "techniques"', () => { - expect( - createRulesSchema.validate< - Partial> & { - threats: Array>>; - } - >({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'severity', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - threats: [ - { - framework: 'fake', - tactic: { - id: 'fakeId', - name: 'fakeName', - reference: 'fakeRef', - }, - }, - ], - }).error - ).toBeTruthy(); - }); - - test('You can optionally send in an array of false positives', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - false_positives: ['false_1', 'false_2'], - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'severity', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - }).error - ).toBeFalsy(); - }); - - test('You cannot send in an array of false positives that are numbers', () => { - expect( - createRulesSchema.validate< - Partial> & { false_positives: number[] } - >({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - false_positives: [5, 4], - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'severity', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - }).error - ).toBeTruthy(); - }); - - test('You can optionally set the immutable to be true', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - immutable: true, - index: ['index-1'], - name: 'some-name', - severity: 'severity', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - }).error - ).toBeFalsy(); - }); - - test('You cannot set the immutable to be a number', () => { - expect( - createRulesSchema.validate< - Partial> & { immutable: number } - >({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - immutable: 5, - index: ['index-1'], - name: 'some-name', - severity: 'severity', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - }).error - ).toBeTruthy(); - }); - - test('You cannot set the risk_score to 101', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 101, - description: 'some description', - from: 'now-5m', - to: 'now', - immutable: true, - index: ['index-1'], - name: 'some-name', - severity: 'severity', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - }).error - ).toBeTruthy(); - }); - - test('You cannot set the risk_score to -1', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: -1, - description: 'some description', - from: 'now-5m', - to: 'now', - immutable: true, - index: ['index-1'], - name: 'some-name', - severity: 'severity', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - }).error - ).toBeTruthy(); - }); - - test('You can set the risk_score to 0', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 0, - description: 'some description', - from: 'now-5m', - to: 'now', - immutable: true, - index: ['index-1'], - name: 'some-name', - severity: 'severity', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - }).error - ).toBeFalsy(); - }); - - test('You can set the risk_score to 100', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 100, - description: 'some description', - from: 'now-5m', - to: 'now', - immutable: true, - index: ['index-1'], - name: 'some-name', - severity: 'severity', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - }).error - ).toBeFalsy(); - }); - - test('You can set meta to any object you want', () => { - expect( - createRulesSchema.validate>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - immutable: true, - index: ['index-1'], - name: 'some-name', - severity: 'severity', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - meta: { - somethingMadeUp: { somethingElse: true }, - }, - }).error - ).toBeFalsy(); - }); - - test('You cannot create meta as a string', () => { - expect( - createRulesSchema.validate & { meta: string }>>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - immutable: true, - index: ['index-1'], - name: 'some-name', - severity: 'severity', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - meta: 'should not work', - }).error - ).toBeTruthy(); - }); - - test('You can omit the query string when filters are present', () => { - expect( - createRulesSchema.validate & { meta: string }>>({ - rule_id: 'rule-1', - output_index: '.siem-signals', - risk_score: 50, - description: 'some description', - from: 'now-5m', - to: 'now', - immutable: true, - index: ['index-1'], - name: 'some-name', - severity: 'severity', - interval: '5m', - type: 'query', - references: ['index-1'], - language: 'kuery', - filters: [], - max_signals: 1, - }).error - ).toBeFalsy(); - }); - }); - - describe('update rules schema', () => { - test('empty objects do not validate as they require at least id or rule_id', () => { - expect(updateRulesSchema.validate>({}).error).toBeTruthy(); - }); - - test('made up values do not validate', () => { - expect( - updateRulesSchema.validate>({ - madeUp: 'hi', - }).error - ).toBeTruthy(); - }); - - test('[id] does validate', () => { - expect( - updateRulesSchema.validate>({ - id: 'rule-1', - }).error - ).toBeFalsy(); - }); - - test('[rule_id] does validate', () => { - expect( - updateRulesSchema.validate>({ - rule_id: 'rule-1', - }).error - ).toBeFalsy(); - }); - - test('[id and rule_id] does not validate', () => { - expect( - updateRulesSchema.validate>({ - id: 'id-1', - rule_id: 'rule-1', - }).error - ).toBeTruthy(); - }); - - test('[rule_id, description] does validate', () => { - expect( - updateRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - }).error - ).toBeFalsy(); - }); - - test('[id, description] does validate', () => { - expect( - updateRulesSchema.validate>({ - id: 'rule-1', - description: 'some description', - }).error - ).toBeFalsy(); - }); - - test('[id, risk_score] does validate', () => { - expect( - updateRulesSchema.validate>({ - id: 'rule-1', - risk_score: 10, - }).error - ).toBeFalsy(); - }); - - test('[rule_id, description, from] does validate', () => { - expect( - updateRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - }).error - ).toBeFalsy(); - }); - - test('[id, description, from] does validate', () => { - expect( - updateRulesSchema.validate>({ - id: 'rule-1', - description: 'some description', - from: 'now-5m', - }).error - ).toBeFalsy(); - }); - - test('[rule_id, description, from, to] does validate', () => { - expect( - updateRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - }).error - ).toBeFalsy(); - }); - - test('[id, description, from, to] does validate', () => { - expect( - updateRulesSchema.validate>({ - id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - }).error - ).toBeFalsy(); - }); - - test('[rule_id, description, from, to, name] does validate', () => { - expect( - updateRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - name: 'some-name', - }).error - ).toBeFalsy(); - }); - - test('[id, description, from, to, name] does validate', () => { - expect( - updateRulesSchema.validate>({ - id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - name: 'some-name', - }).error - ).toBeFalsy(); - }); - - test('[rule_id, description, from, to, name, severity] does validate', () => { - expect( - updateRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - name: 'some-name', - severity: 'severity', - }).error - ).toBeFalsy(); - }); - - test('[id, description, from, to, name, severity] does validate', () => { - expect( - updateRulesSchema.validate>({ - id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - name: 'some-name', - severity: 'severity', - }).error - ).toBeFalsy(); - }); - - test('[rule_id, description, from, to, name, severity, type] does validate', () => { - expect( - updateRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - name: 'some-name', - severity: 'severity', - type: 'query', - }).error - ).toBeFalsy(); - }); - - test('[id, description, from, to, name, severity, type] does validate', () => { - expect( - updateRulesSchema.validate>({ - id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - name: 'some-name', - severity: 'severity', - type: 'query', - }).error - ).toBeFalsy(); - }); - - test('[rule_id, description, from, to, name, severity, type, interval] does validate', () => { - expect( - updateRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - name: 'some-name', - severity: 'severity', - interval: '5m', - type: 'query', - }).error - ).toBeFalsy(); - }); - - test('[id, description, from, to, name, severity, type, interval] does validate', () => { - expect( - updateRulesSchema.validate>({ - id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - name: 'some-name', - severity: 'severity', - interval: '5m', - type: 'query', - }).error - ).toBeFalsy(); - }); - - test('[rule_id, description, from, to, index, name, severity, interval, type] does validate', () => { - expect( - updateRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'severity', - interval: '5m', - type: 'query', - }).error - ).toBeFalsy(); - }); - - test('[id, description, from, to, index, name, severity, interval, type] does validate', () => { - expect( - updateRulesSchema.validate>({ - id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'severity', - interval: '5m', - type: 'query', - }).error - ).toBeFalsy(); - }); - - test('[rule_id, description, from, to, index, name, severity, interval, type, query] does validate', () => { - expect( - updateRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'severity', - interval: '5m', - type: 'query', - query: 'some query', - }).error - ).toBeFalsy(); - }); - - test('[id, description, from, to, index, name, severity, interval, type, query] does validate', () => { - expect( - updateRulesSchema.validate>({ - id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'severity', - interval: '5m', - type: 'query', - query: 'some query', - }).error - ).toBeFalsy(); - }); - - test('[rule_id, description, from, to, index, name, severity, interval, type, query, language] does validate', () => { - expect( - updateRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'severity', - interval: '5m', - type: 'query', - query: 'some query', - language: 'kuery', - }).error - ).toBeFalsy(); - }); - - test('[id, description, from, to, index, name, severity, interval, type, query, language] does validate', () => { - expect( - updateRulesSchema.validate>({ - id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'severity', - interval: '5m', - type: 'query', - query: 'some query', - language: 'kuery', - }).error - ).toBeFalsy(); - }); - - test('[rule_id, description, from, to, index, name, severity, type, filter] does validate', () => { - expect( - updateRulesSchema.validate>({ - rule_id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'severity', - interval: '5m', - type: 'query', - }).error - ).toBeFalsy(); - }); - - test('[id, description, from, to, index, name, severity, type, filter] does validate', () => { - expect( - updateRulesSchema.validate>({ - id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'severity', - interval: '5m', - type: 'query', - }).error - ).toBeFalsy(); - }); - - test('allows references to be sent as a valid value to update with', () => { - expect( - updateRulesSchema.validate>({ - id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'severity', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - }).error - ).toBeFalsy(); - }); - - test('does not default references to an array', () => { - expect( - updateRulesSchema.validate>({ - id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'severity', - interval: '5m', - type: 'query', - query: 'some-query', - language: 'kuery', - }).value.references - ).toEqual(undefined); - }); - - test('does not default interval', () => { - expect( - updateRulesSchema.validate>({ - id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'severity', - type: 'query', - }).value.interval - ).toEqual(undefined); - }); - - test('does not default max signal', () => { - expect( - updateRulesSchema.validate>({ - id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'severity', - interval: '5m', - type: 'query', - }).value.max_signals - ).toEqual(undefined); - }); - - test('references cannot be numbers', () => { - expect( - updateRulesSchema.validate< - Partial> & { references: number[] } - >({ - id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'severity', - interval: '5m', - type: 'query', - query: 'some-query', - language: 'kuery', - references: [5], - }).error - ).toBeTruthy(); - }); - - test('indexes cannot be numbers', () => { - expect( - updateRulesSchema.validate< - Partial> & { index: number[] } - >({ - id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: [5], - name: 'some-name', - severity: 'severity', - interval: '5m', - type: 'query', - query: 'some-query', - language: 'kuery', - }).error - ).toBeTruthy(); - }); - - test('saved_id is not required when type is saved_query and will validate without it', () => { - expect( - updateRulesSchema.validate>({ - id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'severity', - interval: '5m', - type: 'saved_query', - }).error - ).toBeFalsy(); - }); - - test('saved_id validates with saved_query', () => { - expect( - updateRulesSchema.validate>({ - id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'severity', - interval: '5m', - type: 'saved_query', - saved_id: 'some id', - }).error - ).toBeFalsy(); - }); - - test('saved_query type can have filters with it', () => { - expect( - updateRulesSchema.validate>({ - id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'severity', - interval: '5m', - type: 'saved_query', - saved_id: 'some id', - filters: [], - }).error - ).toBeFalsy(); - }); - - test('language validates with kuery', () => { - expect( - updateRulesSchema.validate>({ - id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'severity', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - }).error - ).toBeFalsy(); - }); - - test('language validates with lucene', () => { - expect( - updateRulesSchema.validate>({ - id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'severity', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'lucene', - }).error - ).toBeFalsy(); - }); - - test('language does not validate with something made up', () => { - expect( - updateRulesSchema.validate>({ - id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'severity', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'something-made-up', - }).error - ).toBeTruthy(); - }); - - test('max_signals cannot be negative', () => { - expect( - updateRulesSchema.validate>({ - id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'severity', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: -1, - }).error - ).toBeTruthy(); - }); - - test('max_signals cannot be zero', () => { - expect( - updateRulesSchema.validate>({ - id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'severity', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 0, - }).error - ).toBeTruthy(); - }); - - test('max_signals can be 1', () => { - expect( - updateRulesSchema.validate>({ - id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'severity', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - }).error - ).toBeFalsy(); - }); - - test('meta can be updated', () => { - expect( - updateRulesSchema.validate>({ - id: 'rule-1', - meta: { whateverYouWant: 'anything_at_all' }, - }).error - ).toBeFalsy(); - }); - - test('You update meta as a string', () => { - expect( - updateRulesSchema.validate< - Partial & { meta: string }> - >({ - id: 'rule-1', - meta: 'should not work', - }).error - ).toBeTruthy(); - }); - - test('filters cannot be a string', () => { - expect( - updateRulesSchema.validate< - Partial & { filters: string }> - >({ - rule_id: 'rule-1', - type: 'query', - filters: 'some string', - }).error - ).toBeTruthy(); - }); - - test('threats is not defaulted to empty array on update', () => { - expect( - updateRulesSchema.validate>({ - id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'severity', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - }).value.threats - ).toBe(undefined); - }); - - test('threats is not defaulted to undefined on update with empty array', () => { - expect( - updateRulesSchema.validate>({ - id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'severity', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - threats: [], - }).value.threats - ).toMatchObject([]); - }); - test('threats is valid when updated with all sub-objects', () => { - const expected: ThreatParams[] = [ - { - framework: 'fake', - tactic: { - id: 'fakeId', - name: 'fakeName', - reference: 'fakeRef', - }, - techniques: [ - { - id: 'techniqueId', - name: 'techniqueName', - reference: 'techniqueRef', - }, - ], - }, - ]; - expect( - updateRulesSchema.validate>({ - id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'severity', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - threats: [ - { - framework: 'fake', - tactic: { - id: 'fakeId', - name: 'fakeName', - reference: 'fakeRef', - }, - techniques: [ - { - id: 'techniqueId', - name: 'techniqueName', - reference: 'techniqueRef', - }, - ], - }, - ], - }).value.threats - ).toMatchObject(expected); - }); - test('threats is invalid when updated with missing property framework', () => { - expect( - updateRulesSchema.validate< - Partial> & { - threats: Array>>; - } - >({ - id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'severity', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - threats: [ - { - tactic: { - id: 'fakeId', - name: 'fakeName', - reference: 'fakeRef', - }, - techniques: [ - { - id: 'techniqueId', - name: 'techniqueName', - reference: 'techniqueRef', - }, - ], - }, - ], - }).error - ).toBeTruthy(); - }); - test('threats is invalid when updated with missing tactic sub-object', () => { - expect( - updateRulesSchema.validate< - Partial> & { - threats: Array>>; - } - >({ - id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'severity', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - threats: [ - { - framework: 'fake', - techniques: [ - { - id: 'techniqueId', - name: 'techniqueName', - reference: 'techniqueRef', - }, - ], - }, - ], - }).error - ).toBeTruthy(); - }); - test('threats is invalid when updated with missing techniques', () => { - expect( - updateRulesSchema.validate< - Partial> & { - threats: Array>>; - } - >({ - id: 'rule-1', - description: 'some description', - from: 'now-5m', - to: 'now', - index: ['index-1'], - name: 'some-name', - severity: 'severity', - interval: '5m', - type: 'query', - references: ['index-1'], - query: 'some query', - language: 'kuery', - max_signals: 1, - threats: [ - { - framework: 'fake', - tactic: { - id: 'techniqueId', - name: 'techniqueName', - reference: 'techniqueRef', - }, - }, - ], - }).error - ).toBeTruthy(); - }); - }); - - describe('find rules schema', () => { - test('empty objects do validate', () => { - expect(findRulesSchema.validate>({}).error).toBeFalsy(); - }); - - test('all values validate', () => { - expect( - findRulesSchema.validate>({ - per_page: 5, - page: 1, - sort_field: 'some field', - fields: ['field 1', 'field 2'], - filter: 'some filter', - sort_order: 'asc', - }).error - ).toBeFalsy(); - }); - - test('made up parameters do not validate', () => { - expect( - findRulesSchema.validate>({ - madeUp: 'hi', - }).error - ).toBeTruthy(); - }); - - test('per_page validates', () => { - expect( - findRulesSchema.validate>({ per_page: 5 }).error - ).toBeFalsy(); - }); - - test('page validates', () => { - expect( - findRulesSchema.validate>({ page: 5 }).error - ).toBeFalsy(); - }); - - test('sort_field validates', () => { - expect( - findRulesSchema.validate>({ sort_field: 'some value' }).error - ).toBeFalsy(); - }); - - test('fields validates with a string', () => { - expect( - findRulesSchema.validate>({ fields: ['some value'] }).error - ).toBeFalsy(); - }); - - test('fields validates with multiple strings', () => { - expect( - findRulesSchema.validate>({ - fields: ['some value 1', 'some value 2'], - }).error - ).toBeFalsy(); - }); - - test('fields does not validate with a number', () => { - expect( - findRulesSchema.validate> & { fields: number[] }>({ - fields: [5], - }).error - ).toBeTruthy(); - }); - - test('per page has a default of 20', () => { - expect(findRulesSchema.validate>({}).value.per_page).toEqual(20); - }); - - test('page has a default of 1', () => { - expect(findRulesSchema.validate>({}).value.page).toEqual(1); - }); - - test('filter works with a string', () => { - expect( - findRulesSchema.validate>({ - filter: 'some value 1', - }).error - ).toBeFalsy(); - }); - - test('filter does not work with a number', () => { - expect( - findRulesSchema.validate> & { filter: number }>({ - filter: 5, - }).error - ).toBeTruthy(); - }); - - test('sort_order requires sort_field to work', () => { - expect( - findRulesSchema.validate>({ - sort_order: 'asc', - }).error - ).toBeTruthy(); - }); - - test('sort_order and sort_field validate together', () => { - expect( - findRulesSchema.validate>({ - sort_order: 'asc', - sort_field: 'some field', - }).error - ).toBeFalsy(); - }); - - test('sort_order validates with desc and sort_field', () => { - expect( - findRulesSchema.validate>({ - sort_order: 'desc', - sort_field: 'some field', - }).error - ).toBeFalsy(); - }); - - test('sort_order does not validate with a string other than asc and desc', () => { - expect( - findRulesSchema.validate< - Partial> & { sort_order: string } - >({ - sort_order: 'some other string', - sort_field: 'some field', - }).error - ).toBeTruthy(); - }); - }); - - describe('queryRulesSchema', () => { - test('empty objects do not validate', () => { - expect(queryRulesSchema.validate>({}).error).toBeTruthy(); - }); - - test('both rule_id and id being supplied dot not validate', () => { - expect( - queryRulesSchema.validate>({ rule_id: '1', id: '1' }) - .error - ).toBeTruthy(); - }); - - test('only id validates', () => { - expect( - queryRulesSchema.validate>({ id: '1' }).error - ).toBeFalsy(); - }); - - test('only rule_id validates', () => { - expect( - queryRulesSchema.validate>({ rule_id: '1' }).error - ).toBeFalsy(); - }); - }); - - describe('set signal status schema', () => { - test('signal_ids and status is valid', () => { - expect( - setSignalsStatusSchema.validate>({ - signal_ids: ['somefakeid'], - status: 'open', - }).error - ).toBeFalsy(); - }); - - test('query and status is valid', () => { - expect( - setSignalsStatusSchema.validate>({ - query: {}, - status: 'open', - }).error - ).toBeFalsy(); - }); - - test('signal_ids and missing status is invalid', () => { - expect( - setSignalsStatusSchema.validate>({ - signal_ids: ['somefakeid'], - }).error - ).toBeTruthy(); - }); - - test('query and missing status is invalid', () => { - expect( - setSignalsStatusSchema.validate>({ - query: {}, - }).error - ).toBeTruthy(); - }); - - test('status is present but query or signal_ids is missing is invalid', () => { - expect( - setSignalsStatusSchema.validate>({ - status: 'closed', - }).error - ).toBeTruthy(); - }); - - test('signal_ids is present but status has wrong value', () => { - expect( - setSignalsStatusSchema.validate< - Partial< - Omit & { - status: string; - } - > - >({ - status: 'fakeVal', - }).error - ).toBeTruthy(); - }); - }); -}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.ts deleted file mode 100644 index 6ed6fdd2577d8..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.ts +++ /dev/null @@ -1,162 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import Joi from 'joi'; -import { DEFAULT_MAX_SIGNALS } from '../../../../common/constants'; - -/* eslint-disable @typescript-eslint/camelcase */ -const description = Joi.string(); -const enabled = Joi.boolean(); -const false_positives = Joi.array().items(Joi.string()); -const filters = Joi.array(); -const from = Joi.string(); -const immutable = Joi.boolean(); -const rule_id = Joi.string(); -const id = Joi.string(); -const index = Joi.array() - .items(Joi.string()) - .single(); -const interval = Joi.string(); -const query = Joi.string(); -const language = Joi.string().valid('kuery', 'lucene'); -const output_index = Joi.string(); -const saved_id = Joi.string(); -const meta = Joi.object(); -const max_signals = Joi.number().greater(0); -const name = Joi.string(); -const risk_score = Joi.number() - .greater(-1) - .less(101); -const severity = Joi.string(); -const status = Joi.string().valid('open', 'closed'); -const to = Joi.string(); -const type = Joi.string().valid('query', 'saved_query'); -const queryFilter = Joi.string(); -const references = Joi.array() - .items(Joi.string()) - .single(); -const per_page = Joi.number() - .min(0) - .default(20); -const page = Joi.number() - .min(1) - .default(1); -const signal_ids = Joi.array().items(Joi.string()); -const signal_status_query = Joi.object(); -const sort_field = Joi.string(); -const sort_order = Joi.string().valid('asc', 'desc'); -const tags = Joi.array().items(Joi.string()); -const fields = Joi.array() - .items(Joi.string()) - .single(); -const threat_framework = Joi.string(); -const threat_tactic_id = Joi.string(); -const threat_tactic_name = Joi.string(); -const threat_tactic_reference = Joi.string(); -const threat_tactic = Joi.object({ - id: threat_tactic_id.required(), - name: threat_tactic_name.required(), - reference: threat_tactic_reference.required(), -}); -const threat_technique_id = Joi.string(); -const threat_technique_name = Joi.string(); -const threat_technique_reference = Joi.string(); -const threat_technique = Joi.object({ - id: threat_technique_id.required(), - name: threat_technique_name.required(), - reference: threat_technique_reference.required(), -}); -const threat_techniques = Joi.array().items(threat_technique.required()); - -const threats = Joi.array().items( - Joi.object({ - framework: threat_framework.required(), - tactic: threat_tactic.required(), - techniques: threat_techniques.required(), - }) -); -/* eslint-enable @typescript-eslint/camelcase */ - -export const createRulesSchema = Joi.object({ - description: description.required(), - enabled: enabled.default(true), - false_positives: false_positives.default([]), - filters, - from: from.required(), - rule_id, - immutable: immutable.default(false), - index, - interval: interval.default('5m'), - query: query.allow('').default(''), - language: language.default('kuery'), - output_index, - saved_id: saved_id.when('type', { - is: 'saved_query', - then: Joi.required(), - otherwise: Joi.forbidden(), - }), - meta, - risk_score: risk_score.required(), - max_signals: max_signals.default(DEFAULT_MAX_SIGNALS), - name: name.required(), - severity: severity.required(), - tags: tags.default([]), - to: to.required(), - type: type.required(), - threats: threats.default([]), - references: references.default([]), -}); - -export const updateRulesSchema = Joi.object({ - description, - enabled, - false_positives, - filters, - from, - rule_id, - id, - immutable, - index, - interval, - query: query.allow(''), - language, - output_index, - saved_id, - meta, - risk_score, - max_signals, - name, - severity, - tags, - to, - type, - threats, - references, -}).xor('id', 'rule_id'); - -export const queryRulesSchema = Joi.object({ - rule_id, - id, -}).xor('id', 'rule_id'); - -export const findRulesSchema = Joi.object({ - fields, - filter: queryFilter, - per_page, - page, - sort_field: Joi.when(Joi.ref('sort_order'), { - is: Joi.exist(), - then: sort_field.required(), - otherwise: sort_field.optional(), - }), - sort_order, -}); - -export const setSignalsStatusSchema = Joi.object({ - signal_ids, - query: signal_status_query, - status: status.required(), -}).xor('signal_ids', 'query'); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.test.ts new file mode 100644 index 0000000000000..4efea69db1f41 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.test.ts @@ -0,0 +1,1047 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createRulesSchema } from './create_rules_schema'; +import { UpdateRuleAlertParamsRest } from '../../rules/types'; +import { ThreatParams, RuleAlertParamsRest } from '../../types'; + +describe('create rules schema', () => { + test('empty objects do not validate', () => { + expect(createRulesSchema.validate>({}).error).toBeTruthy(); + }); + + test('made up values do not validate', () => { + expect( + createRulesSchema.validate>({ + madeUp: 'hi', + }).error + ).toBeTruthy(); + }); + + test('[rule_id] does not validate', () => { + expect( + createRulesSchema.validate>({ + rule_id: 'rule-1', + }).error + ).toBeTruthy(); + }); + + test('[rule_id, description] does not validate', () => { + expect( + createRulesSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + }).error + ).toBeTruthy(); + }); + + test('[rule_id, description, from] does not validate', () => { + expect( + createRulesSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + }).error + ).toBeTruthy(); + }); + + test('[rule_id, description, from, to] does not validate', () => { + expect( + createRulesSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + }).error + ).toBeTruthy(); + }); + + test('[rule_id, description, from, to, name] does not validate', () => { + expect( + createRulesSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + }).error + ).toBeTruthy(); + }); + + test('[rule_id, description, from, to, name, severity] does not validate', () => { + expect( + createRulesSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + severity: 'severity', + }).error + ).toBeTruthy(); + }); + + test('[rule_id, description, from, to, name, severity, type] does not validate', () => { + expect( + createRulesSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + severity: 'severity', + type: 'query', + }).error + ).toBeTruthy(); + }); + + test('[rule_id, description, from, to, name, severity, type, interval] does not validate', () => { + expect( + createRulesSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + }).error + ).toBeTruthy(); + }); + + test('[rule_id, description, from, to, name, severity, type, interval, index] does not validate', () => { + expect( + createRulesSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + severity: 'severity', + type: 'query', + interval: '5m', + index: ['index-1'], + }).error + ).toBeTruthy(); + }); + + test('[rule_id, description, from, to, name, severity, type, query, index, interval] does validate', () => { + expect( + createRulesSchema.validate>({ + rule_id: 'rule-1', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + severity: 'severity', + type: 'query', + query: 'some query', + index: ['index-1'], + interval: '5m', + }).error + ).toBeFalsy(); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, query, language] does not validate', () => { + expect( + createRulesSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + query: 'some query', + language: 'kuery', + }).error + ).toBeTruthy(); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, query, language, risk_score] does validate', () => { + expect( + createRulesSchema.validate>({ + rule_id: 'rule-1', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + query: 'some query', + language: 'kuery', + }).error + ).toBeFalsy(); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, query, language, risk_score, output_index] does validate', () => { + expect( + createRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + query: 'some query', + language: 'kuery', + }).error + ).toBeFalsy(); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score] does validate', () => { + expect( + createRulesSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + risk_score: 50, + }).error + ).toBeFalsy(); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, output_index] does validate', () => { + expect( + createRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + }).error + ).toBeFalsy(); + }); + test('You can send in an empty array to threats', () => { + expect( + createRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + threats: [], + }).error + ).toBeFalsy(); + }); + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, output_index, threats] does validate', () => { + expect( + createRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + threats: [ + { + framework: 'someFramework', + tactic: { + id: 'fakeId', + name: 'fakeName', + reference: 'fakeRef', + }, + techniques: [ + { + id: 'techniqueId', + name: 'techniqueName', + reference: 'techniqueRef', + }, + ], + }, + ], + }).error + ).toBeFalsy(); + }); + + test('allows references to be sent as valid', () => { + expect( + createRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + }).error + ).toBeFalsy(); + }); + + test('defaults references to an array', () => { + expect( + createRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + query: 'some-query', + language: 'kuery', + }).value.references + ).toEqual([]); + }); + + test('references cannot be numbers', () => { + expect( + createRulesSchema.validate< + Partial> & { references: number[] } + >({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + query: 'some-query', + language: 'kuery', + references: [5], + }).error + ).toBeTruthy(); + }); + + test('indexes cannot be numbers', () => { + expect( + createRulesSchema.validate> & { index: number[] }>( + { + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: [5], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + query: 'some-query', + language: 'kuery', + } + ).error + ).toBeTruthy(); + }); + + test('defaults interval to 5 min', () => { + expect( + createRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + type: 'query', + }).value.interval + ).toEqual('5m'); + }); + + test('defaults max signals to 100', () => { + expect( + createRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + }).value.max_signals + ).toEqual(100); + }); + + test('saved_id is required when type is saved_query and will not validate without out', () => { + expect( + createRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'saved_query', + }).error + ).toBeTruthy(); + }); + + test('saved_id is required when type is saved_query and validates with it', () => { + expect( + createRulesSchema.validate>({ + rule_id: 'rule-1', + risk_score: 50, + output_index: '.siem-signals', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'saved_query', + saved_id: 'some id', + }).error + ).toBeFalsy(); + }); + + test('saved_query type can have filters with it', () => { + expect( + createRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'saved_query', + saved_id: 'some id', + filters: [], + }).error + ).toBeFalsy(); + }); + + test('filters cannot be a string', () => { + expect( + createRulesSchema.validate< + Partial & { filters: string }> + >({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'saved_query', + saved_id: 'some id', + filters: 'some string', + }).error + ).toBeTruthy(); + }); + + test('language validates with kuery', () => { + expect( + createRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + }).error + ).toBeFalsy(); + }); + + test('language validates with lucene', () => { + expect( + createRulesSchema.validate>({ + rule_id: 'rule-1', + risk_score: 50, + output_index: '.siem-signals', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'lucene', + }).error + ).toBeFalsy(); + }); + + test('language does not validate with something made up', () => { + expect( + createRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'something-made-up', + }).error + ).toBeTruthy(); + }); + + test('max_signals cannot be negative', () => { + expect( + createRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: -1, + }).error + ).toBeTruthy(); + }); + + test('max_signals cannot be zero', () => { + expect( + createRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 0, + }).error + ).toBeTruthy(); + }); + + test('max_signals can be 1', () => { + expect( + createRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + }).error + ).toBeFalsy(); + }); + + test('You can optionally send in an array of tags', () => { + expect( + createRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + tags: ['tag_1', 'tag_2'], + }).error + ).toBeFalsy(); + }); + + test('You cannot send in an array of tags that are numbers', () => { + expect( + createRulesSchema.validate> & { tags: number[] }>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + tags: [0, 1, 2], + }).error + ).toBeTruthy(); + }); + + test('You cannot send in an array of threats that are missing "framework"', () => { + expect( + createRulesSchema.validate< + Partial> & { + threats: Array>>; + } + >({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + threats: [ + { + tactic: { + id: 'fakeId', + name: 'fakeName', + reference: 'fakeRef', + }, + techniques: [ + { + id: 'techniqueId', + name: 'techniqueName', + reference: 'techniqueRef', + }, + ], + }, + ], + }).error + ).toBeTruthy(); + }); + test('You cannot send in an array of threats that are missing "tactic"', () => { + expect( + createRulesSchema.validate< + Partial> & { + threats: Array>>; + } + >({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + threats: [ + { + framework: 'fake', + techniques: [ + { + id: 'techniqueId', + name: 'techniqueName', + reference: 'techniqueRef', + }, + ], + }, + ], + }).error + ).toBeTruthy(); + }); + test('You cannot send in an array of threats that are missing "techniques"', () => { + expect( + createRulesSchema.validate< + Partial> & { + threats: Array>>; + } + >({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + threats: [ + { + framework: 'fake', + tactic: { + id: 'fakeId', + name: 'fakeName', + reference: 'fakeRef', + }, + }, + ], + }).error + ).toBeTruthy(); + }); + + test('You can optionally send in an array of false positives', () => { + expect( + createRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + false_positives: ['false_1', 'false_2'], + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + }).error + ).toBeFalsy(); + }); + + test('You cannot send in an array of false positives that are numbers', () => { + expect( + createRulesSchema.validate< + Partial> & { false_positives: number[] } + >({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + false_positives: [5, 4], + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + }).error + ).toBeTruthy(); + }); + + test('You can optionally set the immutable to be true', () => { + expect( + createRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + immutable: true, + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + }).error + ).toBeFalsy(); + }); + + test('You cannot set the immutable to be a number', () => { + expect( + createRulesSchema.validate< + Partial> & { immutable: number } + >({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + immutable: 5, + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + }).error + ).toBeTruthy(); + }); + + test('You cannot set the risk_score to 101', () => { + expect( + createRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 101, + description: 'some description', + from: 'now-5m', + to: 'now', + immutable: true, + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + }).error + ).toBeTruthy(); + }); + + test('You cannot set the risk_score to -1', () => { + expect( + createRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: -1, + description: 'some description', + from: 'now-5m', + to: 'now', + immutable: true, + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + }).error + ).toBeTruthy(); + }); + + test('You can set the risk_score to 0', () => { + expect( + createRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 0, + description: 'some description', + from: 'now-5m', + to: 'now', + immutable: true, + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + }).error + ).toBeFalsy(); + }); + + test('You can set the risk_score to 100', () => { + expect( + createRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 100, + description: 'some description', + from: 'now-5m', + to: 'now', + immutable: true, + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + }).error + ).toBeFalsy(); + }); + + test('You can set meta to any object you want', () => { + expect( + createRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + immutable: true, + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + meta: { + somethingMadeUp: { somethingElse: true }, + }, + }).error + ).toBeFalsy(); + }); + + test('You cannot create meta as a string', () => { + expect( + createRulesSchema.validate & { meta: string }>>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + immutable: true, + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + meta: 'should not work', + }).error + ).toBeTruthy(); + }); + + test('You can omit the query string when filters are present', () => { + expect( + createRulesSchema.validate & { meta: string }>>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + immutable: true, + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + language: 'kuery', + filters: [], + max_signals: 1, + }).error + ).toBeFalsy(); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.ts new file mode 100644 index 0000000000000..ccda7256d2eeb --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.ts @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Joi from 'joi'; + +/* eslint-disable @typescript-eslint/camelcase */ +import { + enabled, + description, + false_positives, + filters, + from, + immutable, + index, + rule_id, + interval, + query, + language, + output_index, + saved_id, + meta, + risk_score, + max_signals, + name, + severity, + tags, + to, + type, + threats, + references, +} from './schemas'; +/* eslint-enable @typescript-eslint/camelcase */ + +import { DEFAULT_MAX_SIGNALS } from '../../../../../common/constants'; + +export const createRulesSchema = Joi.object({ + description: description.required(), + enabled: enabled.default(true), + false_positives: false_positives.default([]), + filters, + from: from.required(), + rule_id, + immutable: immutable.default(false), + index, + interval: interval.default('5m'), + query: query.allow('').default(''), + language: language.default('kuery'), + output_index, + saved_id: saved_id.when('type', { + is: 'saved_query', + then: Joi.required(), + otherwise: Joi.forbidden(), + }), + meta, + risk_score: risk_score.required(), + max_signals: max_signals.default(DEFAULT_MAX_SIGNALS), + name: name.required(), + severity: severity.required(), + tags: tags.default([]), + to: to.required(), + type: type.required(), + threats: threats.default([]), + references: references.default([]), +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/find_rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/find_rules_schema.test.ts new file mode 100644 index 0000000000000..14b3bdb298739 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/find_rules_schema.test.ts @@ -0,0 +1,136 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { findRulesSchema } from './find_rules_schema'; +import { FindParamsRest } from '../../rules/types'; + +describe('find rules schema', () => { + test('empty objects do validate', () => { + expect(findRulesSchema.validate>({}).error).toBeFalsy(); + }); + + test('all values validate', () => { + expect( + findRulesSchema.validate>({ + per_page: 5, + page: 1, + sort_field: 'some field', + fields: ['field 1', 'field 2'], + filter: 'some filter', + sort_order: 'asc', + }).error + ).toBeFalsy(); + }); + + test('made up parameters do not validate', () => { + expect( + findRulesSchema.validate>({ + madeUp: 'hi', + }).error + ).toBeTruthy(); + }); + + test('per_page validates', () => { + expect( + findRulesSchema.validate>({ per_page: 5 }).error + ).toBeFalsy(); + }); + + test('page validates', () => { + expect( + findRulesSchema.validate>({ page: 5 }).error + ).toBeFalsy(); + }); + + test('sort_field validates', () => { + expect( + findRulesSchema.validate>({ sort_field: 'some value' }).error + ).toBeFalsy(); + }); + + test('fields validates with a string', () => { + expect( + findRulesSchema.validate>({ fields: ['some value'] }).error + ).toBeFalsy(); + }); + + test('fields validates with multiple strings', () => { + expect( + findRulesSchema.validate>({ + fields: ['some value 1', 'some value 2'], + }).error + ).toBeFalsy(); + }); + + test('fields does not validate with a number', () => { + expect( + findRulesSchema.validate> & { fields: number[] }>({ + fields: [5], + }).error + ).toBeTruthy(); + }); + + test('per page has a default of 20', () => { + expect(findRulesSchema.validate>({}).value.per_page).toEqual(20); + }); + + test('page has a default of 1', () => { + expect(findRulesSchema.validate>({}).value.page).toEqual(1); + }); + + test('filter works with a string', () => { + expect( + findRulesSchema.validate>({ + filter: 'some value 1', + }).error + ).toBeFalsy(); + }); + + test('filter does not work with a number', () => { + expect( + findRulesSchema.validate> & { filter: number }>({ + filter: 5, + }).error + ).toBeTruthy(); + }); + + test('sort_order requires sort_field to work', () => { + expect( + findRulesSchema.validate>({ + sort_order: 'asc', + }).error + ).toBeTruthy(); + }); + + test('sort_order and sort_field validate together', () => { + expect( + findRulesSchema.validate>({ + sort_order: 'asc', + sort_field: 'some field', + }).error + ).toBeFalsy(); + }); + + test('sort_order validates with desc and sort_field', () => { + expect( + findRulesSchema.validate>({ + sort_order: 'desc', + sort_field: 'some field', + }).error + ).toBeFalsy(); + }); + + test('sort_order does not validate with a string other than asc and desc', () => { + expect( + findRulesSchema.validate< + Partial> & { sort_order: string } + >({ + sort_order: 'some other string', + sort_field: 'some field', + }).error + ).toBeTruthy(); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/find_rules_schema.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/find_rules_schema.ts new file mode 100644 index 0000000000000..3cc5b9ca44530 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/find_rules_schema.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Joi from 'joi'; + +/* eslint-disable @typescript-eslint/camelcase */ +import { queryFilter, fields, per_page, page, sort_field, sort_order } from './schemas'; +/* eslint-enable @typescript-eslint/camelcase */ + +export const findRulesSchema = Joi.object({ + fields, + filter: queryFilter, + per_page, + page, + sort_field: Joi.when(Joi.ref('sort_order'), { + is: Joi.exist(), + then: sort_field.required(), + otherwise: sort_field.optional(), + }), + sort_order, +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_rules_schema.test.ts new file mode 100644 index 0000000000000..6c4e96abd2b98 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_rules_schema.test.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { queryRulesSchema } from './query_rules_schema'; +import { UpdateRuleAlertParamsRest } from '../../rules/types'; + +describe('queryRulesSchema', () => { + test('empty objects do not validate', () => { + expect(queryRulesSchema.validate>({}).error).toBeTruthy(); + }); + + test('both rule_id and id being supplied dot not validate', () => { + expect( + queryRulesSchema.validate>({ rule_id: '1', id: '1' }).error + ).toBeTruthy(); + }); + + test('only id validates', () => { + expect( + queryRulesSchema.validate>({ id: '1' }).error + ).toBeFalsy(); + }); + + test('only rule_id validates', () => { + expect( + queryRulesSchema.validate>({ rule_id: '1' }).error + ).toBeFalsy(); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_rules_schema.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_rules_schema.ts new file mode 100644 index 0000000000000..86a731699d1ea --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_rules_schema.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Joi from 'joi'; + +/* eslint-disable @typescript-eslint/camelcase */ +import { rule_id, id } from './schemas'; +/* eslint-enable @typescript-eslint/camelcase */ + +export const queryRulesSchema = Joi.object({ + rule_id, + id, +}).xor('id', 'rule_id'); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/schemas.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/schemas.ts new file mode 100644 index 0000000000000..5ab8ea3b8af3e --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/schemas.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import Joi from 'joi'; + +/* eslint-disable @typescript-eslint/camelcase */ +export const description = Joi.string(); +export const enabled = Joi.boolean(); +export const false_positives = Joi.array().items(Joi.string()); +export const filters = Joi.array(); +export const from = Joi.string(); +export const immutable = Joi.boolean(); +export const rule_id = Joi.string(); +export const id = Joi.string(); +export const index = Joi.array() + .items(Joi.string()) + .single(); +export const interval = Joi.string(); +export const query = Joi.string(); +export const language = Joi.string().valid('kuery', 'lucene'); +export const output_index = Joi.string(); +export const saved_id = Joi.string(); +export const meta = Joi.object(); +export const max_signals = Joi.number().greater(0); +export const name = Joi.string(); +export const risk_score = Joi.number() + .greater(-1) + .less(101); +export const severity = Joi.string(); +export const status = Joi.string().valid('open', 'closed'); +export const to = Joi.string(); +export const type = Joi.string().valid('query', 'saved_query'); +export const queryFilter = Joi.string(); +export const references = Joi.array() + .items(Joi.string()) + .single(); +export const per_page = Joi.number() + .min(0) + .default(20); +export const page = Joi.number() + .min(1) + .default(1); +export const signal_ids = Joi.array().items(Joi.string()); +export const signal_status_query = Joi.object(); +export const sort_field = Joi.string(); +export const sort_order = Joi.string().valid('asc', 'desc'); +export const tags = Joi.array().items(Joi.string()); +export const fields = Joi.array() + .items(Joi.string()) + .single(); +export const threat_framework = Joi.string(); +export const threat_tactic_id = Joi.string(); +export const threat_tactic_name = Joi.string(); +export const threat_tactic_reference = Joi.string(); +export const threat_tactic = Joi.object({ + id: threat_tactic_id.required(), + name: threat_tactic_name.required(), + reference: threat_tactic_reference.required(), +}); +export const threat_technique_id = Joi.string(); +export const threat_technique_name = Joi.string(); +export const threat_technique_reference = Joi.string(); +export const threat_technique = Joi.object({ + id: threat_technique_id.required(), + name: threat_technique_name.required(), + reference: threat_technique_reference.required(), +}); +export const threat_techniques = Joi.array().items(threat_technique.required()); + +export const threats = Joi.array().items( + Joi.object({ + framework: threat_framework.required(), + tactic: threat_tactic.required(), + techniques: threat_techniques.required(), + }) +); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/set_signal_status_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/set_signal_status_schema.test.ts new file mode 100644 index 0000000000000..b586b4666bfee --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/set_signal_status_schema.test.ts @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { setSignalsStatusSchema } from './set_signal_status_schema'; +import { SignalsRestParams } from '../../signals/types'; + +describe('set signal status schema', () => { + test('signal_ids and status is valid', () => { + expect( + setSignalsStatusSchema.validate>({ + signal_ids: ['somefakeid'], + status: 'open', + }).error + ).toBeFalsy(); + }); + + test('query and status is valid', () => { + expect( + setSignalsStatusSchema.validate>({ + query: {}, + status: 'open', + }).error + ).toBeFalsy(); + }); + + test('signal_ids and missing status is invalid', () => { + expect( + setSignalsStatusSchema.validate>({ + signal_ids: ['somefakeid'], + }).error + ).toBeTruthy(); + }); + + test('query and missing status is invalid', () => { + expect( + setSignalsStatusSchema.validate>({ + query: {}, + }).error + ).toBeTruthy(); + }); + + test('status is present but query or signal_ids is missing is invalid', () => { + expect( + setSignalsStatusSchema.validate>({ + status: 'closed', + }).error + ).toBeTruthy(); + }); + + test('signal_ids is present but status has wrong value', () => { + expect( + setSignalsStatusSchema.validate< + Partial< + Omit & { + status: string; + } + > + >({ + status: 'fakeVal', + }).error + ).toBeTruthy(); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/set_signal_status_schema.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/set_signal_status_schema.ts new file mode 100644 index 0000000000000..c8a06619287df --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/set_signal_status_schema.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Joi from 'joi'; + +/* eslint-disable @typescript-eslint/camelcase */ +import { signal_ids, signal_status_query, status } from './schemas'; +/* eslint-enable @typescript-eslint/camelcase */ + +export const setSignalsStatusSchema = Joi.object({ + signal_ids, + query: signal_status_query, + status: status.required(), +}).xor('signal_ids', 'query'); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.test.ts new file mode 100644 index 0000000000000..606a30309b2ab --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.test.ts @@ -0,0 +1,869 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { updateRulesSchema } from './update_rules_schema'; +import { UpdateRuleAlertParamsRest } from '../../rules/types'; +import { ThreatParams } from '../../types'; + +describe('update rules schema', () => { + test('empty objects do not validate as they require at least id or rule_id', () => { + expect(updateRulesSchema.validate>({}).error).toBeTruthy(); + }); + + test('made up values do not validate', () => { + expect( + updateRulesSchema.validate>({ + madeUp: 'hi', + }).error + ).toBeTruthy(); + }); + + test('[id] does validate', () => { + expect( + updateRulesSchema.validate>({ + id: 'rule-1', + }).error + ).toBeFalsy(); + }); + + test('[rule_id] does validate', () => { + expect( + updateRulesSchema.validate>({ + rule_id: 'rule-1', + }).error + ).toBeFalsy(); + }); + + test('[id and rule_id] does not validate', () => { + expect( + updateRulesSchema.validate>({ + id: 'id-1', + rule_id: 'rule-1', + }).error + ).toBeTruthy(); + }); + + test('[rule_id, description] does validate', () => { + expect( + updateRulesSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + }).error + ).toBeFalsy(); + }); + + test('[id, description] does validate', () => { + expect( + updateRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + }).error + ).toBeFalsy(); + }); + + test('[id, risk_score] does validate', () => { + expect( + updateRulesSchema.validate>({ + id: 'rule-1', + risk_score: 10, + }).error + ).toBeFalsy(); + }); + + test('[rule_id, description, from] does validate', () => { + expect( + updateRulesSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + }).error + ).toBeFalsy(); + }); + + test('[id, description, from] does validate', () => { + expect( + updateRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + }).error + ).toBeFalsy(); + }); + + test('[rule_id, description, from, to] does validate', () => { + expect( + updateRulesSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + }).error + ).toBeFalsy(); + }); + + test('[id, description, from, to] does validate', () => { + expect( + updateRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + }).error + ).toBeFalsy(); + }); + + test('[rule_id, description, from, to, name] does validate', () => { + expect( + updateRulesSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + }).error + ).toBeFalsy(); + }); + + test('[id, description, from, to, name] does validate', () => { + expect( + updateRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + }).error + ).toBeFalsy(); + }); + + test('[rule_id, description, from, to, name, severity] does validate', () => { + expect( + updateRulesSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + severity: 'severity', + }).error + ).toBeFalsy(); + }); + + test('[id, description, from, to, name, severity] does validate', () => { + expect( + updateRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + severity: 'severity', + }).error + ).toBeFalsy(); + }); + + test('[rule_id, description, from, to, name, severity, type] does validate', () => { + expect( + updateRulesSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + severity: 'severity', + type: 'query', + }).error + ).toBeFalsy(); + }); + + test('[id, description, from, to, name, severity, type] does validate', () => { + expect( + updateRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + severity: 'severity', + type: 'query', + }).error + ).toBeFalsy(); + }); + + test('[rule_id, description, from, to, name, severity, type, interval] does validate', () => { + expect( + updateRulesSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + }).error + ).toBeFalsy(); + }); + + test('[id, description, from, to, name, severity, type, interval] does validate', () => { + expect( + updateRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + }).error + ).toBeFalsy(); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type] does validate', () => { + expect( + updateRulesSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + }).error + ).toBeFalsy(); + }); + + test('[id, description, from, to, index, name, severity, interval, type] does validate', () => { + expect( + updateRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + }).error + ).toBeFalsy(); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, query] does validate', () => { + expect( + updateRulesSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + query: 'some query', + }).error + ).toBeFalsy(); + }); + + test('[id, description, from, to, index, name, severity, interval, type, query] does validate', () => { + expect( + updateRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + query: 'some query', + }).error + ).toBeFalsy(); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, query, language] does validate', () => { + expect( + updateRulesSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + query: 'some query', + language: 'kuery', + }).error + ).toBeFalsy(); + }); + + test('[id, description, from, to, index, name, severity, interval, type, query, language] does validate', () => { + expect( + updateRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + query: 'some query', + language: 'kuery', + }).error + ).toBeFalsy(); + }); + + test('[rule_id, description, from, to, index, name, severity, type, filter] does validate', () => { + expect( + updateRulesSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + }).error + ).toBeFalsy(); + }); + + test('[id, description, from, to, index, name, severity, type, filter] does validate', () => { + expect( + updateRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + }).error + ).toBeFalsy(); + }); + + test('allows references to be sent as a valid value to update with', () => { + expect( + updateRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + }).error + ).toBeFalsy(); + }); + + test('does not default references to an array', () => { + expect( + updateRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + query: 'some-query', + language: 'kuery', + }).value.references + ).toEqual(undefined); + }); + + test('does not default interval', () => { + expect( + updateRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + type: 'query', + }).value.interval + ).toEqual(undefined); + }); + + test('does not default max signal', () => { + expect( + updateRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + }).value.max_signals + ).toEqual(undefined); + }); + + test('references cannot be numbers', () => { + expect( + updateRulesSchema.validate< + Partial> & { references: number[] } + >({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + query: 'some-query', + language: 'kuery', + references: [5], + }).error + ).toBeTruthy(); + }); + + test('indexes cannot be numbers', () => { + expect( + updateRulesSchema.validate< + Partial> & { index: number[] } + >({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: [5], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + query: 'some-query', + language: 'kuery', + }).error + ).toBeTruthy(); + }); + + test('saved_id is not required when type is saved_query and will validate without it', () => { + expect( + updateRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'saved_query', + }).error + ).toBeFalsy(); + }); + + test('saved_id validates with saved_query', () => { + expect( + updateRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'saved_query', + saved_id: 'some id', + }).error + ).toBeFalsy(); + }); + + test('saved_query type can have filters with it', () => { + expect( + updateRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'saved_query', + saved_id: 'some id', + filters: [], + }).error + ).toBeFalsy(); + }); + + test('language validates with kuery', () => { + expect( + updateRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + }).error + ).toBeFalsy(); + }); + + test('language validates with lucene', () => { + expect( + updateRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'lucene', + }).error + ).toBeFalsy(); + }); + + test('language does not validate with something made up', () => { + expect( + updateRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'something-made-up', + }).error + ).toBeTruthy(); + }); + + test('max_signals cannot be negative', () => { + expect( + updateRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: -1, + }).error + ).toBeTruthy(); + }); + + test('max_signals cannot be zero', () => { + expect( + updateRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 0, + }).error + ).toBeTruthy(); + }); + + test('max_signals can be 1', () => { + expect( + updateRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + }).error + ).toBeFalsy(); + }); + + test('meta can be updated', () => { + expect( + updateRulesSchema.validate>({ + id: 'rule-1', + meta: { whateverYouWant: 'anything_at_all' }, + }).error + ).toBeFalsy(); + }); + + test('You update meta as a string', () => { + expect( + updateRulesSchema.validate< + Partial & { meta: string }> + >({ + id: 'rule-1', + meta: 'should not work', + }).error + ).toBeTruthy(); + }); + + test('filters cannot be a string', () => { + expect( + updateRulesSchema.validate< + Partial & { filters: string }> + >({ + rule_id: 'rule-1', + type: 'query', + filters: 'some string', + }).error + ).toBeTruthy(); + }); + + test('threats is not defaulted to empty array on update', () => { + expect( + updateRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + }).value.threats + ).toBe(undefined); + }); + + test('threats is not defaulted to undefined on update with empty array', () => { + expect( + updateRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + threats: [], + }).value.threats + ).toMatchObject([]); + }); + test('threats is valid when updated with all sub-objects', () => { + const expected: ThreatParams[] = [ + { + framework: 'fake', + tactic: { + id: 'fakeId', + name: 'fakeName', + reference: 'fakeRef', + }, + techniques: [ + { + id: 'techniqueId', + name: 'techniqueName', + reference: 'techniqueRef', + }, + ], + }, + ]; + expect( + updateRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + threats: [ + { + framework: 'fake', + tactic: { + id: 'fakeId', + name: 'fakeName', + reference: 'fakeRef', + }, + techniques: [ + { + id: 'techniqueId', + name: 'techniqueName', + reference: 'techniqueRef', + }, + ], + }, + ], + }).value.threats + ).toMatchObject(expected); + }); + test('threats is invalid when updated with missing property framework', () => { + expect( + updateRulesSchema.validate< + Partial> & { + threats: Array>>; + } + >({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + threats: [ + { + tactic: { + id: 'fakeId', + name: 'fakeName', + reference: 'fakeRef', + }, + techniques: [ + { + id: 'techniqueId', + name: 'techniqueName', + reference: 'techniqueRef', + }, + ], + }, + ], + }).error + ).toBeTruthy(); + }); + test('threats is invalid when updated with missing tactic sub-object', () => { + expect( + updateRulesSchema.validate< + Partial> & { + threats: Array>>; + } + >({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + threats: [ + { + framework: 'fake', + techniques: [ + { + id: 'techniqueId', + name: 'techniqueName', + reference: 'techniqueRef', + }, + ], + }, + ], + }).error + ).toBeTruthy(); + }); + test('threats is invalid when updated with missing techniques', () => { + expect( + updateRulesSchema.validate< + Partial> & { + threats: Array>>; + } + >({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + threats: [ + { + framework: 'fake', + tactic: { + id: 'techniqueId', + name: 'techniqueName', + reference: 'techniqueRef', + }, + }, + ], + }).error + ).toBeTruthy(); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.ts new file mode 100644 index 0000000000000..244d8d1f5cc77 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Joi from 'joi'; + +/* eslint-disable @typescript-eslint/camelcase */ +import { + enabled, + description, + false_positives, + filters, + from, + immutable, + index, + rule_id, + interval, + query, + language, + output_index, + saved_id, + meta, + risk_score, + max_signals, + name, + severity, + tags, + to, + type, + threats, + references, + id, +} from './schemas'; +/* eslint-enable @typescript-eslint/camelcase */ + +export const updateRulesSchema = Joi.object({ + description, + enabled, + false_positives, + filters, + from, + rule_id, + id, + immutable, + index, + interval, + query: query.allow(''), + language, + output_index, + saved_id, + meta, + risk_score, + max_signals, + name, + severity, + tags, + to, + type, + threats, + references, +}).xor('id', 'rule_id'); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals_route.ts index 99af43ce51a12..b342cc5cd14ef 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals_route.ts @@ -6,8 +6,8 @@ import Hapi from 'hapi'; import { DETECTION_ENGINE_SIGNALS_STATUS_URL } from '../../../../../common/constants'; -import { SignalsRequest } from '../../alerts/types'; -import { setSignalsStatusSchema } from '../schemas'; +import { SignalsRequest } from '../../signals/types'; +import { setSignalsStatusSchema } from '../schemas/set_signal_status_schema'; import { ServerFacade } from '../../../../types'; import { transformError, getIndex } from '../utils'; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts index 4663ea357f259..8ca5c24d88100 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts @@ -6,495 +6,9 @@ import Boom from 'boom'; -import { - transformAlertToRule, - getIdError, - transformFindAlertsOrError, - transformOrError, - transformError, -} from './utils'; -import { getResult } from './__mocks__/request_responses'; +import { transformError } from './utils'; describe('utils', () => { - describe('transformAlertToRule', () => { - test('should work with a full data set', () => { - const fullRule = getResult(); - const rule = transformAlertToRule(fullRule); - expect(rule).toEqual({ - created_by: 'elastic', - description: 'Detecting root and admin users', - enabled: true, - false_positives: [], - from: 'now-6m', - id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', - immutable: false, - index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - interval: '5m', - risk_score: 50, - rule_id: 'rule-1', - language: 'kuery', - max_signals: 100, - name: 'Detect Root/Admin Users', - output_index: '.siem-signals', - query: 'user.name: root or user.name: admin', - references: ['http://www.example.com', 'https://ww.example.com'], - severity: 'high', - updated_by: 'elastic', - tags: [], - threats: [ - { - framework: 'MITRE ATT&CK', - tactic: { - id: 'TA0040', - name: 'impact', - reference: 'https://attack.mitre.org/tactics/TA0040/', - }, - techniques: [ - { - id: 'T1499', - name: 'endpoint denial of service', - reference: 'https://attack.mitre.org/techniques/T1499/', - }, - ], - }, - ], - to: 'now', - type: 'query', - }); - }); - - test('should work with a partial data set missing data', () => { - const fullRule = getResult(); - const { from, language, ...omitData } = transformAlertToRule(fullRule); - expect(omitData).toEqual({ - created_by: 'elastic', - description: 'Detecting root and admin users', - enabled: true, - false_positives: [], - id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', - immutable: false, - index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - output_index: '.siem-signals', - interval: '5m', - risk_score: 50, - rule_id: 'rule-1', - max_signals: 100, - name: 'Detect Root/Admin Users', - query: 'user.name: root or user.name: admin', - references: ['http://www.example.com', 'https://ww.example.com'], - severity: 'high', - updated_by: 'elastic', - tags: [], - threats: [ - { - framework: 'MITRE ATT&CK', - tactic: { - id: 'TA0040', - name: 'impact', - reference: 'https://attack.mitre.org/tactics/TA0040/', - }, - techniques: [ - { - id: 'T1499', - name: 'endpoint denial of service', - reference: 'https://attack.mitre.org/techniques/T1499/', - }, - ], - }, - ], - to: 'now', - type: 'query', - }); - }); - - test('should omit query if query is null', () => { - const fullRule = getResult(); - fullRule.params.query = null; - const rule = transformAlertToRule(fullRule); - expect(rule).toEqual({ - created_by: 'elastic', - description: 'Detecting root and admin users', - enabled: true, - false_positives: [], - from: 'now-6m', - id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', - immutable: false, - index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - output_index: '.siem-signals', - interval: '5m', - risk_score: 50, - rule_id: 'rule-1', - language: 'kuery', - max_signals: 100, - name: 'Detect Root/Admin Users', - references: ['http://www.example.com', 'https://ww.example.com'], - severity: 'high', - updated_by: 'elastic', - tags: [], - threats: [ - { - framework: 'MITRE ATT&CK', - tactic: { - id: 'TA0040', - name: 'impact', - reference: 'https://attack.mitre.org/tactics/TA0040/', - }, - techniques: [ - { - id: 'T1499', - name: 'endpoint denial of service', - reference: 'https://attack.mitre.org/techniques/T1499/', - }, - ], - }, - ], - to: 'now', - type: 'query', - }); - }); - - test('should omit query if query is undefined', () => { - const fullRule = getResult(); - fullRule.params.query = undefined; - const rule = transformAlertToRule(fullRule); - expect(rule).toEqual({ - created_by: 'elastic', - description: 'Detecting root and admin users', - enabled: true, - false_positives: [], - from: 'now-6m', - id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', - immutable: false, - index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - output_index: '.siem-signals', - interval: '5m', - rule_id: 'rule-1', - risk_score: 50, - language: 'kuery', - max_signals: 100, - name: 'Detect Root/Admin Users', - references: ['http://www.example.com', 'https://ww.example.com'], - severity: 'high', - updated_by: 'elastic', - tags: [], - threats: [ - { - framework: 'MITRE ATT&CK', - tactic: { - id: 'TA0040', - name: 'impact', - reference: 'https://attack.mitre.org/tactics/TA0040/', - }, - techniques: [ - { - id: 'T1499', - name: 'endpoint denial of service', - reference: 'https://attack.mitre.org/techniques/T1499/', - }, - ], - }, - ], - to: 'now', - type: 'query', - }); - }); - - test('should omit a mix of undefined, null, and missing fields', () => { - const fullRule = getResult(); - fullRule.params.query = undefined; - fullRule.params.language = null; - const { from, enabled, ...omitData } = transformAlertToRule(fullRule); - expect(omitData).toEqual({ - created_by: 'elastic', - description: 'Detecting root and admin users', - false_positives: [], - id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', - immutable: false, - output_index: '.siem-signals', - index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - interval: '5m', - rule_id: 'rule-1', - risk_score: 50, - max_signals: 100, - name: 'Detect Root/Admin Users', - references: ['http://www.example.com', 'https://ww.example.com'], - severity: 'high', - updated_by: 'elastic', - tags: [], - threats: [ - { - framework: 'MITRE ATT&CK', - tactic: { - id: 'TA0040', - name: 'impact', - reference: 'https://attack.mitre.org/tactics/TA0040/', - }, - techniques: [ - { - id: 'T1499', - name: 'endpoint denial of service', - reference: 'https://attack.mitre.org/techniques/T1499/', - }, - ], - }, - ], - to: 'now', - type: 'query', - }); - }); - - test('should return enabled is equal to false', () => { - const fullRule = getResult(); - fullRule.enabled = false; - const ruleWithEnabledFalse = transformAlertToRule(fullRule); - expect(ruleWithEnabledFalse).toEqual({ - created_by: 'elastic', - description: 'Detecting root and admin users', - enabled: false, - from: 'now-6m', - false_positives: [], - id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', - immutable: false, - output_index: '.siem-signals', - index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - interval: '5m', - language: 'kuery', - risk_score: 50, - rule_id: 'rule-1', - max_signals: 100, - name: 'Detect Root/Admin Users', - query: 'user.name: root or user.name: admin', - references: ['http://www.example.com', 'https://ww.example.com'], - severity: 'high', - updated_by: 'elastic', - tags: [], - threats: [ - { - framework: 'MITRE ATT&CK', - tactic: { - id: 'TA0040', - name: 'impact', - reference: 'https://attack.mitre.org/tactics/TA0040/', - }, - techniques: [ - { - id: 'T1499', - name: 'endpoint denial of service', - reference: 'https://attack.mitre.org/techniques/T1499/', - }, - ], - }, - ], - to: 'now', - type: 'query', - }); - }); - - test('should return immutable is equal to false', () => { - const fullRule = getResult(); - fullRule.params.immutable = false; - const ruleWithEnabledFalse = transformAlertToRule(fullRule); - expect(ruleWithEnabledFalse).toEqual({ - created_by: 'elastic', - description: 'Detecting root and admin users', - enabled: true, - from: 'now-6m', - false_positives: [], - id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', - immutable: false, - output_index: '.siem-signals', - index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - interval: '5m', - language: 'kuery', - risk_score: 50, - rule_id: 'rule-1', - max_signals: 100, - name: 'Detect Root/Admin Users', - query: 'user.name: root or user.name: admin', - references: ['http://www.example.com', 'https://ww.example.com'], - severity: 'high', - updated_by: 'elastic', - tags: [], - threats: [ - { - framework: 'MITRE ATT&CK', - tactic: { - id: 'TA0040', - name: 'impact', - reference: 'https://attack.mitre.org/tactics/TA0040/', - }, - techniques: [ - { - id: 'T1499', - name: 'endpoint denial of service', - reference: 'https://attack.mitre.org/techniques/T1499/', - }, - ], - }, - ], - to: 'now', - type: 'query', - }); - }); - }); - - describe('getIdError', () => { - test('outputs message about id not being found if only id is defined and ruleId is undefined', () => { - const boom = getIdError({ id: '123', ruleId: undefined }); - expect(boom.message).toEqual('id: "123" not found'); - }); - - test('outputs message about id not being found if only id is defined and ruleId is null', () => { - const boom = getIdError({ id: '123', ruleId: null }); - expect(boom.message).toEqual('id: "123" not found'); - }); - - test('outputs message about ruleId not being found if only ruleId is defined and id is undefined', () => { - const boom = getIdError({ id: undefined, ruleId: 'rule-id-123' }); - expect(boom.message).toEqual('rule_id: "rule-id-123" not found'); - }); - - test('outputs message about ruleId not being found if only ruleId is defined and id is null', () => { - const boom = getIdError({ id: null, ruleId: 'rule-id-123' }); - expect(boom.message).toEqual('rule_id: "rule-id-123" not found'); - }); - - test('outputs message about both being not defined when both are undefined', () => { - const boom = getIdError({ id: undefined, ruleId: undefined }); - expect(boom.message).toEqual('id or rule_id should have been defined'); - }); - - test('outputs message about both being not defined when both are null', () => { - const boom = getIdError({ id: null, ruleId: null }); - expect(boom.message).toEqual('id or rule_id should have been defined'); - }); - - test('outputs message about both being not defined when id is null and ruleId is undefined', () => { - const boom = getIdError({ id: null, ruleId: undefined }); - expect(boom.message).toEqual('id or rule_id should have been defined'); - }); - - test('outputs message about both being not defined when id is undefined and ruleId is null', () => { - const boom = getIdError({ id: undefined, ruleId: null }); - expect(boom.message).toEqual('id or rule_id should have been defined'); - }); - }); - - describe('transformFindAlertsOrError', () => { - test('outputs empty data set when data set is empty correct', () => { - const output = transformFindAlertsOrError({ data: [] }); - expect(output).toEqual({ data: [] }); - }); - - test('outputs 200 if the data is of type siem alert', () => { - const output = transformFindAlertsOrError({ - data: [getResult()], - }); - expect(output).toEqual({ - data: [ - { - created_by: 'elastic', - description: 'Detecting root and admin users', - enabled: true, - false_positives: [], - from: 'now-6m', - id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', - immutable: false, - output_index: '.siem-signals', - index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - interval: '5m', - risk_score: 50, - rule_id: 'rule-1', - language: 'kuery', - max_signals: 100, - name: 'Detect Root/Admin Users', - query: 'user.name: root or user.name: admin', - references: ['http://www.example.com', 'https://ww.example.com'], - severity: 'high', - updated_by: 'elastic', - tags: [], - to: 'now', - type: 'query', - threats: [ - { - framework: 'MITRE ATT&CK', - tactic: { - id: 'TA0040', - name: 'impact', - reference: 'https://attack.mitre.org/tactics/TA0040/', - }, - techniques: [ - { - id: 'T1499', - name: 'endpoint denial of service', - reference: 'https://attack.mitre.org/techniques/T1499/', - }, - ], - }, - ], - }, - ], - }); - }); - - test('returns 500 if the data is not of type siem alert', () => { - const output = transformFindAlertsOrError({ data: [{ random: 1 }] }); - expect((output as Boom).message).toEqual('Internal error transforming'); - }); - }); - - describe('transformOrError', () => { - test('outputs 200 if the data is of type siem alert', () => { - const output = transformOrError(getResult()); - expect(output).toEqual({ - created_by: 'elastic', - description: 'Detecting root and admin users', - enabled: true, - false_positives: [], - from: 'now-6m', - id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', - immutable: false, - output_index: '.siem-signals', - index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - interval: '5m', - rule_id: 'rule-1', - risk_score: 50, - language: 'kuery', - max_signals: 100, - name: 'Detect Root/Admin Users', - query: 'user.name: root or user.name: admin', - references: ['http://www.example.com', 'https://ww.example.com'], - severity: 'high', - updated_by: 'elastic', - tags: [], - to: 'now', - type: 'query', - threats: [ - { - framework: 'MITRE ATT&CK', - tactic: { - id: 'TA0040', - name: 'impact', - reference: 'https://attack.mitre.org/tactics/TA0040/', - }, - techniques: [ - { - id: 'T1499', - name: 'endpoint denial of service', - reference: 'https://attack.mitre.org/techniques/T1499/', - }, - ], - }, - ], - }); - }); - - test('returns 500 if the data is not of type siem alert', () => { - const output = transformOrError({ data: [{ random: 1 }] }); - expect((output as Boom).message).toEqual('Internal error transforming'); - }); - }); - describe('transformError', () => { test('returns boom if it is a boom object', () => { const boom = new Boom(''); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts index 6df4174e628b3..aed0ced5cdeb5 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts @@ -5,77 +5,9 @@ */ import Boom from 'boom'; -import { pickBy } from 'lodash/fp'; import { APP_ID, SIGNALS_INDEX_KEY } from '../../../../common/constants'; -import { RuleAlertType, isAlertType, OutputRuleAlertRest, isAlertTypes } from '../alerts/types'; import { ServerFacade, RequestFacade } from '../../../types'; -export const getIdError = ({ - id, - ruleId, -}: { - id: string | undefined | null; - ruleId: string | undefined | null; -}) => { - if (id != null) { - return new Boom(`id: "${id}" not found`, { statusCode: 404 }); - } else if (ruleId != null) { - return new Boom(`rule_id: "${ruleId}" not found`, { statusCode: 404 }); - } else { - return new Boom(`id or rule_id should have been defined`, { statusCode: 404 }); - } -}; - -// Transforms the data but will remove any null or undefined it encounters and not include -// those on the export -export const transformAlertToRule = (alert: RuleAlertType): Partial => { - return pickBy((value: unknown) => value != null, { - created_by: alert.createdBy, - description: alert.params.description, - enabled: alert.enabled, - false_positives: alert.params.falsePositives, - filters: alert.params.filters, - from: alert.params.from, - id: alert.id, - immutable: alert.params.immutable, - index: alert.params.index, - interval: alert.interval, - rule_id: alert.params.ruleId, - language: alert.params.language, - output_index: alert.params.outputIndex, - max_signals: alert.params.maxSignals, - risk_score: alert.params.riskScore, - name: alert.name, - query: alert.params.query, - references: alert.params.references, - saved_id: alert.params.savedId, - meta: alert.params.meta, - severity: alert.params.severity, - updated_by: alert.updatedBy, - tags: alert.tags, - to: alert.params.to, - type: alert.params.type, - threats: alert.params.threats, - }); -}; - -export const transformFindAlertsOrError = (findResults: { data: unknown[] }): unknown | Boom => { - if (isAlertTypes(findResults.data)) { - findResults.data = findResults.data.map(alert => transformAlertToRule(alert)); - return findResults; - } else { - return new Boom('Internal error transforming', { statusCode: 500 }); - } -}; - -export const transformOrError = (alert: unknown): Partial | Boom => { - if (isAlertType(alert)) { - return transformAlertToRule(alert); - } else { - return new Boom('Internal error transforming', { statusCode: 500 }); - } -}; - export const transformError = (err: Error & { statusCode?: number }) => { if (Boom.isBoom(err)) { return err; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/create_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/create_rules.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/delete_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/delete_rules.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/delete_rules.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/delete_rules.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/find_rules.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/find_rules.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/find_rules.test.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/find_rules.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/find_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/find_rules.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/find_rules.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/find_rules.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_rules.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/read_rules.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_rules.test.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/read_rules.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/read_rules.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/read_rules.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/read_rules.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts new file mode 100644 index 0000000000000..5c0fa76b52620 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts @@ -0,0 +1,102 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { get } from 'lodash/fp'; + +import { SIGNALS_ID } from '../../../../common/constants'; +import { AlertsClient } from '../../../../../alerting/server/alerts_client'; +import { ActionsClient } from '../../../../../actions/server/actions_client'; +import { RuleAlertParams, RuleTypeParams, RuleAlertParamsRest } from '../types'; +import { RequestFacade } from '../../../types'; +import { Alert } from '../../../../../alerting/server/types'; + +export type UpdateRuleAlertParamsRest = Partial & { + id: string | undefined; + rule_id: RuleAlertParams['ruleId'] | undefined; +}; + +export interface FindParamsRest { + per_page: number; + page: number; + sort_field: string; + sort_order: 'asc' | 'desc'; + fields: string[]; + filter: string; +} + +export interface UpdateRulesRequest extends RequestFacade { + payload: UpdateRuleAlertParamsRest; +} + +export type RuleAlertType = Alert & { + id: string; + params: RuleTypeParams; +}; + +export interface RulesRequest extends RequestFacade { + payload: RuleAlertParamsRest; +} + +export interface FindRuleParams { + alertsClient: AlertsClient; + perPage?: number; + page?: number; + sortField?: string; + filter?: string; + fields?: string[]; + sortOrder?: 'asc' | 'desc'; +} + +export interface FindRulesRequest extends Omit { + query: { + per_page: number; + page: number; + search?: string; + sort_field?: string; + filter?: string; + fields?: string[]; + sort_order?: 'asc' | 'desc'; + }; +} + +export interface Clients { + alertsClient: AlertsClient; + actionsClient: ActionsClient; +} + +export type UpdateRuleParams = Partial & { + id: string | undefined | null; +} & Clients; + +export type DeleteRuleParams = Clients & { + id: string | undefined; + ruleId: string | undefined | null; +}; + +export type RuleParams = RuleAlertParams & Clients; + +export interface ReadRuleParams { + alertsClient: AlertsClient; + id?: string | undefined | null; + ruleId?: string | undefined | null; +} + +export interface ReadRuleByRuleId { + alertsClient: AlertsClient; + ruleId: string; +} + +export const isAlertTypes = (obj: unknown[]): obj is RuleAlertType[] => { + return obj.every(rule => isAlertType(rule)); +}; + +export const isAlertType = (obj: unknown): obj is RuleAlertType => { + return get('alertTypeId', obj) === SIGNALS_ID; +}; + +export const isAlertTypeArray = (objArray: unknown[]): objArray is RuleAlertType[] => { + return objArray.length === 0 || isAlertType(objArray[0]); +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_rules.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_rules.test.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_rules.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/__mocks__/es_results.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/__mocks__/es_results.ts similarity index 94% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/__mocks__/es_results.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/__mocks__/es_results.ts index 4c113544e6e21..215d9da6eb7ff 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/__mocks__/es_results.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/__mocks__/es_results.ts @@ -4,12 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - SignalSourceHit, - SignalSearchResponse, - RuleTypeParams, - OutputRuleAlertRest, -} from '../types'; +import { SignalSourceHit, SignalSearchResponse } from '../types'; +import { Logger } from 'kibana/server'; +import { RuleTypeParams, OutputRuleAlertRest } from '../../types'; export const sampleRuleAlertParams = ( maxSignals?: number | undefined, @@ -281,3 +278,13 @@ export const sampleRule = (): Partial => { type: 'query', }; }; + +export const mockLogger: Logger = { + log: jest.fn(), + trace: jest.fn(), + debug: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + fatal: jest.fn(), +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_bulk_body.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_bulk_body.test.ts new file mode 100644 index 0000000000000..e10158a0b879e --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_bulk_body.test.ts @@ -0,0 +1,284 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + sampleRuleAlertParams, + sampleDocNoSortId, + sampleRuleGuid, + sampleIdGuid, +} from './__mocks__/es_results'; +import { buildBulkBody } from './build_bulk_body'; + +describe('buildBulkBody', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('if bulk body builds well-defined body', () => { + const sampleParams = sampleRuleAlertParams(); + const fakeSignalSourceHit = buildBulkBody({ + doc: sampleDocNoSortId(), + ruleParams: sampleParams, + id: sampleRuleGuid, + name: 'rule-name', + createdBy: 'elastic', + updatedBy: 'elastic', + interval: '5m', + enabled: true, + tags: ['some fake tag 1', 'some fake tag 2'], + }); + // Timestamp will potentially always be different so remove it for the test + delete fakeSignalSourceHit['@timestamp']; + expect(fakeSignalSourceHit).toEqual({ + someKey: 'someValue', + event: { + kind: 'signal', + }, + signal: { + parent: { + id: sampleIdGuid, + type: 'event', + index: 'myFakeSignalIndex', + depth: 1, + }, + original_time: 'someTimeStamp', + status: 'open', + rule: { + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + rule_id: 'rule-1', + false_positives: [], + max_signals: 10000, + risk_score: 50, + output_index: '.siem-signals', + description: 'Detecting root and admin users', + from: 'now-6m', + immutable: false, + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + interval: '5m', + language: 'kuery', + name: 'rule-name', + query: 'user.name: root or user.name: admin', + references: ['http://google.com'], + severity: 'high', + tags: ['some fake tag 1', 'some fake tag 2'], + type: 'query', + to: 'now', + enabled: true, + created_by: 'elastic', + updated_by: 'elastic', + }, + }, + }); + }); + + test('if bulk body builds original_event if it exists on the event to begin with', () => { + const sampleParams = sampleRuleAlertParams(); + const doc = sampleDocNoSortId(); + doc._source.event = { + action: 'socket_opened', + module: 'system', + dataset: 'socket', + kind: 'event', + }; + const fakeSignalSourceHit = buildBulkBody({ + doc, + ruleParams: sampleParams, + id: sampleRuleGuid, + name: 'rule-name', + createdBy: 'elastic', + updatedBy: 'elastic', + interval: '5m', + enabled: true, + tags: ['some fake tag 1', 'some fake tag 2'], + }); + // Timestamp will potentially always be different so remove it for the test + delete fakeSignalSourceHit['@timestamp']; + expect(fakeSignalSourceHit).toEqual({ + someKey: 'someValue', + event: { + action: 'socket_opened', + dataset: 'socket', + kind: 'signal', + module: 'system', + }, + signal: { + original_event: { + action: 'socket_opened', + dataset: 'socket', + kind: 'event', + module: 'system', + }, + parent: { + id: sampleIdGuid, + type: 'event', + index: 'myFakeSignalIndex', + depth: 1, + }, + original_time: 'someTimeStamp', + status: 'open', + rule: { + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + rule_id: 'rule-1', + false_positives: [], + max_signals: 10000, + risk_score: 50, + output_index: '.siem-signals', + description: 'Detecting root and admin users', + from: 'now-6m', + immutable: false, + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + interval: '5m', + language: 'kuery', + name: 'rule-name', + query: 'user.name: root or user.name: admin', + references: ['http://google.com'], + severity: 'high', + tags: ['some fake tag 1', 'some fake tag 2'], + type: 'query', + to: 'now', + enabled: true, + created_by: 'elastic', + updated_by: 'elastic', + }, + }, + }); + }); + + test('if bulk body builds original_event if it exists on the event to begin with but no kind information', () => { + const sampleParams = sampleRuleAlertParams(); + const doc = sampleDocNoSortId(); + doc._source.event = { + action: 'socket_opened', + module: 'system', + dataset: 'socket', + }; + const fakeSignalSourceHit = buildBulkBody({ + doc, + ruleParams: sampleParams, + id: sampleRuleGuid, + name: 'rule-name', + createdBy: 'elastic', + updatedBy: 'elastic', + interval: '5m', + enabled: true, + tags: ['some fake tag 1', 'some fake tag 2'], + }); + // Timestamp will potentially always be different so remove it for the test + delete fakeSignalSourceHit['@timestamp']; + expect(fakeSignalSourceHit).toEqual({ + someKey: 'someValue', + event: { + action: 'socket_opened', + dataset: 'socket', + kind: 'signal', + module: 'system', + }, + signal: { + original_event: { + action: 'socket_opened', + dataset: 'socket', + module: 'system', + }, + parent: { + id: sampleIdGuid, + type: 'event', + index: 'myFakeSignalIndex', + depth: 1, + }, + original_time: 'someTimeStamp', + status: 'open', + rule: { + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + rule_id: 'rule-1', + false_positives: [], + max_signals: 10000, + risk_score: 50, + output_index: '.siem-signals', + description: 'Detecting root and admin users', + from: 'now-6m', + immutable: false, + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + interval: '5m', + language: 'kuery', + name: 'rule-name', + query: 'user.name: root or user.name: admin', + references: ['http://google.com'], + severity: 'high', + tags: ['some fake tag 1', 'some fake tag 2'], + type: 'query', + to: 'now', + enabled: true, + created_by: 'elastic', + updated_by: 'elastic', + }, + }, + }); + }); + + test('if bulk body builds original_event if it exists on the event to begin with with only kind information', () => { + const sampleParams = sampleRuleAlertParams(); + const doc = sampleDocNoSortId(); + doc._source.event = { + kind: 'event', + }; + const fakeSignalSourceHit = buildBulkBody({ + doc, + ruleParams: sampleParams, + id: sampleRuleGuid, + name: 'rule-name', + createdBy: 'elastic', + updatedBy: 'elastic', + interval: '5m', + enabled: true, + tags: ['some fake tag 1', 'some fake tag 2'], + }); + // Timestamp will potentially always be different so remove it for the test + delete fakeSignalSourceHit['@timestamp']; + expect(fakeSignalSourceHit).toEqual({ + someKey: 'someValue', + event: { + kind: 'signal', + }, + signal: { + original_event: { + kind: 'event', + }, + parent: { + id: sampleIdGuid, + type: 'event', + index: 'myFakeSignalIndex', + depth: 1, + }, + original_time: 'someTimeStamp', + status: 'open', + rule: { + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + rule_id: 'rule-1', + false_positives: [], + max_signals: 10000, + risk_score: 50, + output_index: '.siem-signals', + description: 'Detecting root and admin users', + from: 'now-6m', + immutable: false, + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + interval: '5m', + language: 'kuery', + name: 'rule-name', + query: 'user.name: root or user.name: admin', + references: ['http://google.com'], + severity: 'high', + tags: ['some fake tag 1', 'some fake tag 2'], + type: 'query', + to: 'now', + enabled: true, + created_by: 'elastic', + updated_by: 'elastic', + }, + }, + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_bulk_body.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_bulk_body.ts new file mode 100644 index 0000000000000..6d9f442515b2a --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_bulk_body.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SignalSourceHit, SignalHit } from './types'; +import { buildRule } from './build_rule'; +import { buildSignal } from './build_signal'; +import { buildEventTypeSignal } from './build_event_type_signal'; +import { RuleTypeParams } from '../types'; + +interface BuildBulkBodyParams { + doc: SignalSourceHit; + ruleParams: RuleTypeParams; + id: string; + name: string; + createdBy: string; + updatedBy: string; + interval: string; + enabled: boolean; + tags: string[]; +} + +// format search_after result for signals index. +export const buildBulkBody = ({ + doc, + ruleParams, + id, + name, + createdBy, + updatedBy, + interval, + enabled, + tags, +}: BuildBulkBodyParams): SignalHit => { + const rule = buildRule({ + ruleParams, + id, + name, + enabled, + createdBy, + updatedBy, + interval, + tags, + }); + const signal = buildSignal(doc, rule); + const event = buildEventTypeSignal(doc); + const signalHit: SignalHit = { + ...doc._source, + '@timestamp': new Date().toISOString(), + event, + signal, + }; + return signalHit; +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_event_type_signal.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_event_type_signal.test.ts new file mode 100644 index 0000000000000..106a049002e05 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_event_type_signal.test.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { sampleDocNoSortId } from './__mocks__/es_results'; +import { buildEventTypeSignal } from './build_event_type_signal'; + +describe('buildEventTypeSignal', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('it returns the event appended of kind signal if it does not exist', () => { + const doc = sampleDocNoSortId(); + delete doc._source.event; + const eventType = buildEventTypeSignal(doc); + const expected: object = { kind: 'signal' }; + expect(eventType).toEqual(expected); + }); + + test('it returns the event appended of kind signal if it is an empty object', () => { + const doc = sampleDocNoSortId(); + doc._source.event = {}; + const eventType = buildEventTypeSignal(doc); + const expected: object = { kind: 'signal' }; + expect(eventType).toEqual(expected); + }); + + test('it returns the event with kind signal and other properties if they exist', () => { + const doc = sampleDocNoSortId(); + doc._source.event = { + action: 'socket_opened', + module: 'system', + dataset: 'socket', + }; + const eventType = buildEventTypeSignal(doc); + const expected: object = { + action: 'socket_opened', + module: 'system', + dataset: 'socket', + kind: 'signal', + }; + expect(eventType).toEqual(expected); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_event_type_signal.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_event_type_signal.ts new file mode 100644 index 0000000000000..59cdc020c611d --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_event_type_signal.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SignalSourceHit } from './types'; + +export const buildEventTypeSignal = (doc: SignalSourceHit): object => { + if (doc._source.event != null && doc._source.event instanceof Object) { + return { ...doc._source.event, kind: 'signal' }; + } else { + return { kind: 'signal' }; + } +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/build_events_query.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_events_query.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/build_events_query.test.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_events_query.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/build_events_query.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_events_query.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/build_events_query.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_events_query.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_rule.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_rule.test.ts new file mode 100644 index 0000000000000..c12c6fd333f56 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_rule.test.ts @@ -0,0 +1,156 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { buildRule } from './build_rule'; +import { sampleRuleAlertParams, sampleRuleGuid } from './__mocks__/es_results'; +import { OutputRuleAlertRest } from '../types'; + +describe('buildRule', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('it builds a rule as expected with filters present', () => { + const ruleParams = sampleRuleAlertParams(); + ruleParams.filters = [ + { + query: 'host.name: Rebecca', + }, + { + query: 'host.name: Evan', + }, + { + query: 'host.name: Braden', + }, + ]; + const rule = buildRule({ + ruleParams, + name: 'some-name', + id: sampleRuleGuid, + enabled: false, + createdBy: 'elastic', + updatedBy: 'elastic', + interval: 'some interval', + tags: ['some fake tag 1', 'some fake tag 2'], + }); + const expected: Partial = { + created_by: 'elastic', + description: 'Detecting root and admin users', + enabled: false, + false_positives: [], + from: 'now-6m', + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + immutable: false, + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + interval: 'some interval', + language: 'kuery', + max_signals: 10000, + name: 'some-name', + output_index: '.siem-signals', + query: 'user.name: root or user.name: admin', + references: ['http://google.com'], + risk_score: 50, + rule_id: 'rule-1', + severity: 'high', + tags: ['some fake tag 1', 'some fake tag 2'], + to: 'now', + type: 'query', + updated_by: 'elastic', + filters: [ + { + query: 'host.name: Rebecca', + }, + { + query: 'host.name: Evan', + }, + { + query: 'host.name: Braden', + }, + ], + }; + expect(rule).toEqual(expected); + }); + + test('it omits a null value such as if enabled is null if is present', () => { + const ruleParams = sampleRuleAlertParams(); + ruleParams.filters = undefined; + const rule = buildRule({ + ruleParams, + name: 'some-name', + id: sampleRuleGuid, + enabled: true, + createdBy: 'elastic', + updatedBy: 'elastic', + interval: 'some interval', + tags: ['some fake tag 1', 'some fake tag 2'], + }); + const expected: Partial = { + created_by: 'elastic', + description: 'Detecting root and admin users', + enabled: true, + false_positives: [], + from: 'now-6m', + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + immutable: false, + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + interval: 'some interval', + language: 'kuery', + max_signals: 10000, + name: 'some-name', + output_index: '.siem-signals', + query: 'user.name: root or user.name: admin', + references: ['http://google.com'], + risk_score: 50, + rule_id: 'rule-1', + severity: 'high', + tags: ['some fake tag 1', 'some fake tag 2'], + to: 'now', + type: 'query', + updated_by: 'elastic', + }; + expect(rule).toEqual(expected); + }); + + test('it omits a null value such as if filters is undefined if is present', () => { + const ruleParams = sampleRuleAlertParams(); + ruleParams.filters = undefined; + const rule = buildRule({ + ruleParams, + name: 'some-name', + id: sampleRuleGuid, + enabled: true, + createdBy: 'elastic', + updatedBy: 'elastic', + interval: 'some interval', + tags: ['some fake tag 1', 'some fake tag 2'], + }); + const expected: Partial = { + created_by: 'elastic', + description: 'Detecting root and admin users', + enabled: true, + false_positives: [], + from: 'now-6m', + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + immutable: false, + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + interval: 'some interval', + language: 'kuery', + max_signals: 10000, + name: 'some-name', + output_index: '.siem-signals', + query: 'user.name: root or user.name: admin', + references: ['http://google.com'], + risk_score: 50, + rule_id: 'rule-1', + severity: 'high', + tags: ['some fake tag 1', 'some fake tag 2'], + to: 'now', + type: 'query', + updated_by: 'elastic', + }; + expect(rule).toEqual(expected); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_rule.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_rule.ts new file mode 100644 index 0000000000000..64ec989208b6a --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_rule.ts @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { pickBy } from 'lodash/fp'; +import { RuleTypeParams, OutputRuleAlertRest } from '../types'; + +interface BuildRuleParams { + ruleParams: RuleTypeParams; + name: string; + id: string; + enabled: boolean; + createdBy: string; + updatedBy: string; + interval: string; + tags: string[]; +} + +export const buildRule = ({ + ruleParams, + name, + id, + enabled, + createdBy, + updatedBy, + interval, + tags, +}: BuildRuleParams): Partial => { + return pickBy((value: unknown) => value != null, { + id, + rule_id: ruleParams.ruleId, + false_positives: ruleParams.falsePositives, + saved_id: ruleParams.savedId, + meta: ruleParams.meta, + max_signals: ruleParams.maxSignals, + risk_score: ruleParams.riskScore, + output_index: ruleParams.outputIndex, + description: ruleParams.description, + from: ruleParams.from, + immutable: ruleParams.immutable, + index: ruleParams.index, + interval, + language: ruleParams.language, + name, + query: ruleParams.query, + references: ruleParams.references, + severity: ruleParams.severity, + tags, + type: ruleParams.type, + to: ruleParams.to, + enabled, + filters: ruleParams.filters, + created_by: createdBy, + updated_by: updatedBy, + threats: ruleParams.threats, + }); +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_signal.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_signal.test.ts new file mode 100644 index 0000000000000..1c024d0496743 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_signal.test.ts @@ -0,0 +1,111 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { sampleDocNoSortId, sampleRule } from './__mocks__/es_results'; +import { buildSignal } from './build_signal'; +import { OutputRuleAlertRest } from '../types'; +import { Signal } from './types'; + +describe('buildSignal', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('it builds a signal as expected without original_event if event does not exist', () => { + const doc = sampleDocNoSortId('d5e8eb51-a6a0-456d-8a15-4b79bfec3d71'); + delete doc._source.event; + const rule: Partial = sampleRule(); + const signal = buildSignal(doc, rule); + const expected: Signal = { + parent: { + id: 'd5e8eb51-a6a0-456d-8a15-4b79bfec3d71', + type: 'event', + index: 'myFakeSignalIndex', + depth: 1, + }, + original_time: 'someTimeStamp', + status: 'open', + rule: { + created_by: 'elastic', + description: 'Detecting root and admin users', + enabled: true, + false_positives: [], + from: 'now-6m', + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + immutable: false, + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + interval: '5m', + risk_score: 50, + rule_id: 'rule-1', + language: 'kuery', + max_signals: 100, + name: 'Detect Root/Admin Users', + output_index: '.siem-signals', + query: 'user.name: root or user.name: admin', + references: ['http://www.example.com', 'https://ww.example.com'], + severity: 'high', + updated_by: 'elastic', + tags: ['some fake tag 1', 'some fake tag 2'], + to: 'now', + type: 'query', + }, + }; + expect(signal).toEqual(expected); + }); + + test('it builds a signal as expected with original_event if is present', () => { + const doc = sampleDocNoSortId('d5e8eb51-a6a0-456d-8a15-4b79bfec3d71'); + doc._source.event = { + action: 'socket_opened', + dataset: 'socket', + kind: 'event', + module: 'system', + }; + const rule: Partial = sampleRule(); + const signal = buildSignal(doc, rule); + const expected: Signal = { + parent: { + id: 'd5e8eb51-a6a0-456d-8a15-4b79bfec3d71', + type: 'event', + index: 'myFakeSignalIndex', + depth: 1, + }, + original_time: 'someTimeStamp', + original_event: { + action: 'socket_opened', + dataset: 'socket', + kind: 'event', + module: 'system', + }, + status: 'open', + rule: { + created_by: 'elastic', + description: 'Detecting root and admin users', + enabled: true, + false_positives: [], + from: 'now-6m', + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + immutable: false, + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + interval: '5m', + risk_score: 50, + rule_id: 'rule-1', + language: 'kuery', + max_signals: 100, + name: 'Detect Root/Admin Users', + output_index: '.siem-signals', + query: 'user.name: root or user.name: admin', + references: ['http://www.example.com', 'https://ww.example.com'], + severity: 'high', + updated_by: 'elastic', + tags: ['some fake tag 1', 'some fake tag 2'], + to: 'now', + type: 'query', + }, + }; + expect(signal).toEqual(expected); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_signal.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_signal.ts new file mode 100644 index 0000000000000..4131c843297ea --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_signal.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SignalSourceHit, Signal } from './types'; +import { OutputRuleAlertRest } from '../types'; + +export const buildSignal = (doc: SignalSourceHit, rule: Partial): Signal => { + const signal: Signal = { + parent: { + id: doc._id, + type: 'event', + index: doc._index, + depth: 1, + }, + original_time: doc._source['@timestamp'], + status: 'open', + rule, + }; + if (doc._source.event != null) { + return { ...signal, original_event: doc._source.event }; + } + return signal; +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_filter.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_filter.test.ts similarity index 99% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_filter.test.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_filter.test.ts index e1d10e2efdefb..43b5ce4b590a3 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_filter.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_filter.test.ts @@ -7,7 +7,7 @@ import { getQueryFilter, getFilter } from './get_filter'; import { savedObjectsClientMock } from 'src/core/server/mocks'; import { AlertServices } from '../../../../../alerting/server/types'; -import { PartialFilter } from './types'; +import { PartialFilter } from '../types'; describe('get_filter', () => { let savedObjectsClient = savedObjectsClientMock.create(); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_filter.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_filter.ts similarity index 98% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_filter.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_filter.ts index 858f3580f57e8..8a67d0cb5c5b6 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_filter.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_filter.ts @@ -5,7 +5,6 @@ */ import { AlertServices } from '../../../../../alerting/server/types'; -import { RuleAlertParams, PartialFilter } from './types'; import { assertUnreachable } from '../../../utils/build_query'; import { Query, @@ -13,6 +12,7 @@ import { esFilters, IIndexPattern, } from '../../../../../../../../src/plugins/data/server'; +import { PartialFilter, RuleAlertParams } from '../types'; export const getQueryFilter = ( query: string, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_input_output_index.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_input_output_index.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_input_output_index.test.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_input_output_index.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_input_output_index.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_input_output_index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_input_output_index.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_input_output_index.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.test.ts new file mode 100644 index 0000000000000..ac6f840943f18 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.test.ts @@ -0,0 +1,286 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + sampleRuleAlertParams, + sampleEmptyDocSearchResults, + sampleRuleGuid, + mockLogger, + repeatedSearchResultsWithSortId, + sampleBulkCreateDuplicateResult, + sampleDocSearchResultsNoSortId, + sampleDocSearchResultsNoSortIdNoHits, +} from './__mocks__/es_results'; +import { searchAfterAndBulkCreate } from './search_after_bulk_create'; +import { DEFAULT_SIGNALS_INDEX } from '../../../../common/constants'; +import { savedObjectsClientMock } from 'src/core/server/mocks'; +import uuid from 'uuid'; + +export const mockService = { + callCluster: jest.fn(), + alertInstanceFactory: jest.fn(), + savedObjectsClient: savedObjectsClientMock.create(), +}; + +describe('searchAfterAndBulkCreate', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('if successful with empty search results', async () => { + const sampleParams = sampleRuleAlertParams(); + const result = await searchAfterAndBulkCreate({ + someResult: sampleEmptyDocSearchResults, + ruleParams: sampleParams, + services: mockService, + logger: mockLogger, + id: sampleRuleGuid, + signalsIndex: DEFAULT_SIGNALS_INDEX, + name: 'rule-name', + createdBy: 'elastic', + updatedBy: 'elastic', + interval: '5m', + enabled: true, + pageSize: 1, + filter: undefined, + tags: ['some fake tag 1', 'some fake tag 2'], + }); + expect(mockService.callCluster).toHaveBeenCalledTimes(0); + expect(result).toEqual(true); + }); + test('if successful iteration of while loop with maxDocs', async () => { + const sampleParams = sampleRuleAlertParams(30); + const someGuids = Array.from({ length: 13 }).map(x => uuid.v4()); + mockService.callCluster + .mockReturnValueOnce({ + took: 100, + errors: false, + items: [ + { + fakeItemValue: 'fakeItemKey', + }, + ], + }) + .mockReturnValueOnce(repeatedSearchResultsWithSortId(3, 1, someGuids.slice(0, 3))) + .mockReturnValueOnce({ + took: 100, + errors: false, + items: [ + { + fakeItemValue: 'fakeItemKey', + }, + ], + }) + .mockReturnValueOnce(repeatedSearchResultsWithSortId(3, 1, someGuids.slice(3, 6))) + .mockReturnValueOnce({ + took: 100, + errors: false, + items: [ + { + fakeItemValue: 'fakeItemKey', + }, + ], + }); + const result = await searchAfterAndBulkCreate({ + someResult: repeatedSearchResultsWithSortId(3, 1, someGuids.slice(6, 9)), + ruleParams: sampleParams, + services: mockService, + logger: mockLogger, + id: sampleRuleGuid, + signalsIndex: DEFAULT_SIGNALS_INDEX, + name: 'rule-name', + createdBy: 'elastic', + updatedBy: 'elastic', + interval: '5m', + enabled: true, + pageSize: 1, + filter: undefined, + tags: ['some fake tag 1', 'some fake tag 2'], + }); + expect(mockService.callCluster).toHaveBeenCalledTimes(5); + expect(result).toEqual(true); + }); + test('if unsuccessful first bulk create', async () => { + const someGuids = Array.from({ length: 4 }).map(x => uuid.v4()); + const sampleParams = sampleRuleAlertParams(10); + mockService.callCluster.mockReturnValue(sampleBulkCreateDuplicateResult); + const result = await searchAfterAndBulkCreate({ + someResult: repeatedSearchResultsWithSortId(4, 1, someGuids), + ruleParams: sampleParams, + services: mockService, + logger: mockLogger, + id: sampleRuleGuid, + signalsIndex: DEFAULT_SIGNALS_INDEX, + name: 'rule-name', + createdBy: 'elastic', + updatedBy: 'elastic', + interval: '5m', + enabled: true, + pageSize: 1, + filter: undefined, + tags: ['some fake tag 1', 'some fake tag 2'], + }); + expect(mockLogger.error).toHaveBeenCalled(); + expect(result).toEqual(false); + }); + test('if unsuccessful iteration of searchAfterAndBulkCreate due to empty sort ids', async () => { + const sampleParams = sampleRuleAlertParams(); + mockService.callCluster.mockReturnValueOnce({ + took: 100, + errors: false, + items: [ + { + fakeItemValue: 'fakeItemKey', + }, + ], + }); + const result = await searchAfterAndBulkCreate({ + someResult: sampleDocSearchResultsNoSortId(), + ruleParams: sampleParams, + services: mockService, + logger: mockLogger, + id: sampleRuleGuid, + signalsIndex: DEFAULT_SIGNALS_INDEX, + name: 'rule-name', + createdBy: 'elastic', + updatedBy: 'elastic', + interval: '5m', + enabled: true, + pageSize: 1, + filter: undefined, + tags: ['some fake tag 1', 'some fake tag 2'], + }); + expect(mockLogger.error).toHaveBeenCalled(); + expect(result).toEqual(false); + }); + test('if unsuccessful iteration of searchAfterAndBulkCreate due to empty sort ids and 0 total hits', async () => { + const sampleParams = sampleRuleAlertParams(); + mockService.callCluster.mockReturnValueOnce({ + took: 100, + errors: false, + items: [ + { + fakeItemValue: 'fakeItemKey', + }, + ], + }); + const result = await searchAfterAndBulkCreate({ + someResult: sampleDocSearchResultsNoSortIdNoHits(), + ruleParams: sampleParams, + services: mockService, + logger: mockLogger, + id: sampleRuleGuid, + signalsIndex: DEFAULT_SIGNALS_INDEX, + name: 'rule-name', + createdBy: 'elastic', + updatedBy: 'elastic', + interval: '5m', + enabled: true, + pageSize: 1, + filter: undefined, + tags: ['some fake tag 1', 'some fake tag 2'], + }); + expect(result).toEqual(true); + }); + test('if successful iteration of while loop with maxDocs and search after returns results with no sort ids', async () => { + const sampleParams = sampleRuleAlertParams(10); + const someGuids = Array.from({ length: 4 }).map(x => uuid.v4()); + mockService.callCluster + .mockReturnValueOnce({ + took: 100, + errors: false, + items: [ + { + fakeItemValue: 'fakeItemKey', + }, + ], + }) + .mockReturnValueOnce(sampleDocSearchResultsNoSortId()); + const result = await searchAfterAndBulkCreate({ + someResult: repeatedSearchResultsWithSortId(4, 1, someGuids), + ruleParams: sampleParams, + services: mockService, + logger: mockLogger, + id: sampleRuleGuid, + signalsIndex: DEFAULT_SIGNALS_INDEX, + name: 'rule-name', + createdBy: 'elastic', + updatedBy: 'elastic', + interval: '5m', + enabled: true, + pageSize: 1, + filter: undefined, + tags: ['some fake tag 1', 'some fake tag 2'], + }); + expect(result).toEqual(true); + }); + test('if successful iteration of while loop with maxDocs and search after returns empty results with no sort ids', async () => { + const sampleParams = sampleRuleAlertParams(10); + const someGuids = Array.from({ length: 4 }).map(x => uuid.v4()); + mockService.callCluster + .mockReturnValueOnce({ + took: 100, + errors: false, + items: [ + { + fakeItemValue: 'fakeItemKey', + }, + ], + }) + .mockReturnValueOnce(sampleEmptyDocSearchResults); + const result = await searchAfterAndBulkCreate({ + someResult: repeatedSearchResultsWithSortId(4, 1, someGuids), + ruleParams: sampleParams, + services: mockService, + logger: mockLogger, + id: sampleRuleGuid, + signalsIndex: DEFAULT_SIGNALS_INDEX, + name: 'rule-name', + createdBy: 'elastic', + updatedBy: 'elastic', + interval: '5m', + enabled: true, + pageSize: 1, + filter: undefined, + tags: ['some fake tag 1', 'some fake tag 2'], + }); + expect(result).toEqual(true); + }); + test('if returns false when singleSearchAfter throws an exception', async () => { + const sampleParams = sampleRuleAlertParams(10); + const someGuids = Array.from({ length: 4 }).map(x => uuid.v4()); + mockService.callCluster + .mockReturnValueOnce({ + took: 100, + errors: false, + items: [ + { + fakeItemValue: 'fakeItemKey', + }, + ], + }) + .mockImplementation(() => { + throw Error('Fake Error'); + }); + const result = await searchAfterAndBulkCreate({ + someResult: repeatedSearchResultsWithSortId(4, 1, someGuids), + ruleParams: sampleParams, + services: mockService, + logger: mockLogger, + id: sampleRuleGuid, + signalsIndex: DEFAULT_SIGNALS_INDEX, + name: 'rule-name', + createdBy: 'elastic', + updatedBy: 'elastic', + interval: '5m', + enabled: true, + pageSize: 1, + filter: undefined, + tags: ['some fake tag 1', 'some fake tag 2'], + }); + expect(result).toEqual(false); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.ts new file mode 100644 index 0000000000000..fb314e62ba943 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.ts @@ -0,0 +1,135 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { RuleTypeParams } from '../types'; +import { AlertServices } from '../../../../../alerting/server/types'; +import { Logger } from '../../../../../../../../src/core/server'; +import { singleSearchAfter } from './single_search_after'; +import { singleBulkCreate } from './single_bulk_create'; +import { SignalSearchResponse } from './types'; + +interface SearchAfterAndBulkCreateParams { + someResult: SignalSearchResponse; + ruleParams: RuleTypeParams; + services: AlertServices; + logger: Logger; + id: string; + signalsIndex: string; + name: string; + createdBy: string; + updatedBy: string; + interval: string; + enabled: boolean; + pageSize: number; + filter: unknown; + tags: string[]; +} + +// search_after through documents and re-index using bulk endpoint. +export const searchAfterAndBulkCreate = async ({ + someResult, + ruleParams, + services, + logger, + id, + signalsIndex, + filter, + name, + createdBy, + updatedBy, + interval, + enabled, + pageSize, + tags, +}: SearchAfterAndBulkCreateParams): Promise => { + if (someResult.hits.hits.length === 0) { + return true; + } + + logger.debug('[+] starting bulk insertion'); + await singleBulkCreate({ + someResult, + ruleParams, + services, + logger, + id, + signalsIndex, + name, + createdBy, + updatedBy, + interval, + enabled, + tags, + }); + const totalHits = + typeof someResult.hits.total === 'number' ? someResult.hits.total : someResult.hits.total.value; + // maxTotalHitsSize represents the total number of docs to + // query for, no matter the size of each individual page of search results. + // If the total number of hits for the overall search result is greater than + // maxSignals, default to requesting a total of maxSignals, otherwise use the + // totalHits in the response from the searchAfter query. + const maxTotalHitsSize = totalHits >= ruleParams.maxSignals ? ruleParams.maxSignals : totalHits; + + // number of docs in the current search result + let hitsSize = someResult.hits.hits.length; + logger.debug(`first size: ${hitsSize}`); + let sortIds = someResult.hits.hits[0].sort; + if (sortIds == null && totalHits > 0) { + logger.error('sortIds was empty on first search but expected more'); + return false; + } else if (sortIds == null && totalHits === 0) { + return true; + } + let sortId; + if (sortIds != null) { + sortId = sortIds[0]; + } + while (hitsSize < maxTotalHitsSize && hitsSize !== 0) { + try { + logger.debug(`sortIds: ${sortIds}`); + const searchAfterResult: SignalSearchResponse = await singleSearchAfter({ + searchAfterSortId: sortId, + ruleParams, + services, + logger, + filter, + pageSize, // maximum number of docs to receive per search result. + }); + if (searchAfterResult.hits.hits.length === 0) { + return true; + } + hitsSize += searchAfterResult.hits.hits.length; + logger.debug(`size adjusted: ${hitsSize}`); + sortIds = searchAfterResult.hits.hits[0].sort; + if (sortIds == null) { + logger.debug('sortIds was empty on search'); + return true; // no more search results + } + sortId = sortIds[0]; + logger.debug('next bulk index'); + await singleBulkCreate({ + someResult: searchAfterResult, + ruleParams, + services, + logger, + id, + signalsIndex, + name, + createdBy, + updatedBy, + interval, + enabled, + tags, + }); + logger.debug('finished next bulk index'); + } catch (exc) { + logger.error(`[-] search_after and bulk threw an error ${exc}`); + return false; + } + } + logger.debug(`[+] completed bulk index of ${maxTotalHitsSize}`); + return true; +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/rules_alert_type.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts similarity index 96% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/rules_alert_type.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts index 9823d8b3b9bea..37467e405dd8e 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/rules_alert_type.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -13,18 +13,18 @@ import { } from '../../../../common/constants'; import { buildEventsSearchQuery } from './build_events_query'; -import { searchAfterAndBulkCreate } from './utils'; -import { RuleAlertTypeDefinition } from './types'; -import { getFilter } from './get_filter'; import { getInputIndex } from './get_input_output_index'; +import { searchAfterAndBulkCreate } from './search_after_bulk_create'; +import { getFilter } from './get_filter'; +import { SignalRuleAlertTypeDefinition } from './types'; -export const rulesAlertType = ({ +export const signalRulesAlertType = ({ logger, version, }: { logger: Logger; version: string; -}): RuleAlertTypeDefinition => { +}): SignalRuleAlertTypeDefinition => { return { id: SIGNALS_ID, name: 'SIEM Signals', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.test.ts new file mode 100644 index 0000000000000..d58f0a22b763d --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.test.ts @@ -0,0 +1,230 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { generateId } from './utils'; +import { + sampleRuleAlertParams, + sampleDocSearchResultsNoSortId, + mockLogger, + sampleRuleGuid, + sampleDocSearchResultsNoSortIdNoVersion, + sampleEmptyDocSearchResults, + sampleBulkCreateDuplicateResult, +} from './__mocks__/es_results'; +import { savedObjectsClientMock } from 'src/core/server/mocks'; +import { DEFAULT_SIGNALS_INDEX } from '../../../../common/constants'; +import { singleBulkCreate } from './single_bulk_create'; + +export const mockService = { + callCluster: jest.fn(), + alertInstanceFactory: jest.fn(), + savedObjectsClient: savedObjectsClientMock.create(), +}; + +describe('singleBulkCreate', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('create signal id gereateId', () => { + test('two docs with same index, id, and version should have same id', () => { + const findex = 'myfakeindex'; + const fid = 'somefakeid'; + const version = '1'; + const ruleId = 'rule-1'; + // 'myfakeindexsomefakeid1rule-1' + const generatedHash = '342404d620be4344d6d90dd0461d1d1848aec457944d5c5f40cc0cbfedb36679'; + const firstHash = generateId(findex, fid, version, ruleId); + const secondHash = generateId(findex, fid, version, ruleId); + expect(firstHash).toEqual(generatedHash); + expect(secondHash).toEqual(generatedHash); + expect(Buffer.byteLength(firstHash, 'utf8')).toBeLessThan(512); // 512 bytes is maximum size of _id field + expect(Buffer.byteLength(secondHash, 'utf8')).toBeLessThan(512); + }); + test('two docs with different index, id, and version should have different id', () => { + const findex = 'myfakeindex'; + const findex2 = 'mysecondfakeindex'; + const fid = 'somefakeid'; + const version = '1'; + const ruleId = 'rule-1'; + // 'myfakeindexsomefakeid1rule-1' + const firstGeneratedHash = '342404d620be4344d6d90dd0461d1d1848aec457944d5c5f40cc0cbfedb36679'; + // 'mysecondfakeindexsomefakeid1rule-1' + const secondGeneratedHash = + 'a852941273f805ffe9006e574601acc8ae1148d6c0b3f7f8c4785cba8f6b768a'; + const firstHash = generateId(findex, fid, version, ruleId); + const secondHash = generateId(findex2, fid, version, ruleId); + expect(firstHash).toEqual(firstGeneratedHash); + expect(secondHash).toEqual(secondGeneratedHash); + expect(Buffer.byteLength(firstHash, 'utf8')).toBeLessThan(512); // 512 bytes is maximum size of _id field + expect(Buffer.byteLength(secondHash, 'utf8')).toBeLessThan(512); + expect(firstHash).not.toEqual(secondHash); + }); + test('two docs with same index, different id, and same version should have different id', () => { + const findex = 'myfakeindex'; + const fid = 'somefakeid'; + const fid2 = 'somefakeid2'; + const version = '1'; + const ruleId = 'rule-1'; + // 'myfakeindexsomefakeid1rule-1' + const firstGeneratedHash = '342404d620be4344d6d90dd0461d1d1848aec457944d5c5f40cc0cbfedb36679'; + // 'myfakeindexsomefakeid21rule-1' + const secondGeneratedHash = + '7d33faea18159fd010c4b79890620e8b12cdc88ec1d370149d0e5552ce860255'; + const firstHash = generateId(findex, fid, version, ruleId); + const secondHash = generateId(findex, fid2, version, ruleId); + expect(firstHash).toEqual(firstGeneratedHash); + expect(secondHash).toEqual(secondGeneratedHash); + expect(Buffer.byteLength(firstHash, 'utf8')).toBeLessThan(512); // 512 bytes is maximum size of _id field + expect(Buffer.byteLength(secondHash, 'utf8')).toBeLessThan(512); + expect(firstHash).not.toEqual(secondHash); + }); + test('two docs with same index, same id, and different version should have different id', () => { + const findex = 'myfakeindex'; + const fid = 'somefakeid'; + const version = '1'; + const version2 = '2'; + const ruleId = 'rule-1'; + // 'myfakeindexsomefakeid1rule-1' + const firstGeneratedHash = '342404d620be4344d6d90dd0461d1d1848aec457944d5c5f40cc0cbfedb36679'; + // myfakeindexsomefakeid2rule-1' + const secondGeneratedHash = + 'f016f3071fa9df9221d2fb2ba92389d4d388a4347c6ec7a4012c01cb1c640a40'; + const firstHash = generateId(findex, fid, version, ruleId); + const secondHash = generateId(findex, fid, version2, ruleId); + expect(firstHash).toEqual(firstGeneratedHash); + expect(secondHash).toEqual(secondGeneratedHash); + expect(Buffer.byteLength(firstHash, 'utf8')).toBeLessThan(512); // 512 bytes is maximum size of _id field + expect(Buffer.byteLength(secondHash, 'utf8')).toBeLessThan(512); + expect(firstHash).not.toEqual(secondHash); + }); + test('Ensure generated id is less than 512 bytes, even for really really long strings', () => { + const longIndexName = + 'myfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindexmyfakeindex'; + const fid = 'somefakeid'; + const version = '1'; + const ruleId = 'rule-1'; + const firstHash = generateId(longIndexName, fid, version, ruleId); + expect(Buffer.byteLength(firstHash, 'utf8')).toBeLessThan(512); // 512 bytes is maximum size of _id field + }); + test('two docs with same index, same id, same version number, and different rule ids should have different id', () => { + const findex = 'myfakeindex'; + const fid = 'somefakeid'; + const version = '1'; + const ruleId = 'rule-1'; + const ruleId2 = 'rule-2'; + // 'myfakeindexsomefakeid1rule-1' + const firstGeneratedHash = '342404d620be4344d6d90dd0461d1d1848aec457944d5c5f40cc0cbfedb36679'; + // myfakeindexsomefakeid1rule-2' + const secondGeneratedHash = + '1eb04f997086f8b3b143d4d9b18ac178c4a7423f71a5dad9ba8b9e92603c6863'; + const firstHash = generateId(findex, fid, version, ruleId); + const secondHash = generateId(findex, fid, version, ruleId2); + expect(firstHash).toEqual(firstGeneratedHash); + expect(secondHash).toEqual(secondGeneratedHash); + expect(Buffer.byteLength(firstHash, 'utf8')).toBeLessThan(512); // 512 bytes is maximum size of _id field + expect(Buffer.byteLength(secondHash, 'utf8')).toBeLessThan(512); + expect(firstHash).not.toEqual(secondHash); + }); + }); + test('create successful bulk create', async () => { + const sampleParams = sampleRuleAlertParams(); + const sampleSearchResult = sampleDocSearchResultsNoSortId; + mockService.callCluster.mockReturnValueOnce({ + took: 100, + errors: false, + items: [ + { + fakeItemValue: 'fakeItemKey', + }, + ], + }); + const successfulsingleBulkCreate = await singleBulkCreate({ + someResult: sampleSearchResult(), + ruleParams: sampleParams, + services: mockService, + logger: mockLogger, + id: sampleRuleGuid, + signalsIndex: DEFAULT_SIGNALS_INDEX, + name: 'rule-name', + createdBy: 'elastic', + updatedBy: 'elastic', + interval: '5m', + enabled: true, + tags: ['some fake tag 1', 'some fake tag 2'], + }); + expect(successfulsingleBulkCreate).toEqual(true); + }); + test('create successful bulk create with docs with no versioning', async () => { + const sampleParams = sampleRuleAlertParams(); + const sampleSearchResult = sampleDocSearchResultsNoSortIdNoVersion; + mockService.callCluster.mockReturnValueOnce({ + took: 100, + errors: false, + items: [ + { + fakeItemValue: 'fakeItemKey', + }, + ], + }); + const successfulsingleBulkCreate = await singleBulkCreate({ + someResult: sampleSearchResult(), + ruleParams: sampleParams, + services: mockService, + logger: mockLogger, + id: sampleRuleGuid, + signalsIndex: DEFAULT_SIGNALS_INDEX, + name: 'rule-name', + createdBy: 'elastic', + updatedBy: 'elastic', + interval: '5m', + enabled: true, + tags: ['some fake tag 1', 'some fake tag 2'], + }); + expect(successfulsingleBulkCreate).toEqual(true); + }); + test('create unsuccessful bulk create due to empty search results', async () => { + const sampleParams = sampleRuleAlertParams(); + const sampleSearchResult = sampleEmptyDocSearchResults; + mockService.callCluster.mockReturnValue(false); + const successfulsingleBulkCreate = await singleBulkCreate({ + someResult: sampleSearchResult, + ruleParams: sampleParams, + services: mockService, + logger: mockLogger, + id: sampleRuleGuid, + signalsIndex: DEFAULT_SIGNALS_INDEX, + name: 'rule-name', + createdBy: 'elastic', + updatedBy: 'elastic', + interval: '5m', + enabled: true, + tags: ['some fake tag 1', 'some fake tag 2'], + }); + expect(successfulsingleBulkCreate).toEqual(true); + }); + test('create successful bulk create when bulk create has errors', async () => { + const sampleParams = sampleRuleAlertParams(); + const sampleSearchResult = sampleDocSearchResultsNoSortId; + mockService.callCluster.mockReturnValue(sampleBulkCreateDuplicateResult); + const successfulsingleBulkCreate = await singleBulkCreate({ + someResult: sampleSearchResult(), + ruleParams: sampleParams, + services: mockService, + logger: mockLogger, + id: sampleRuleGuid, + signalsIndex: DEFAULT_SIGNALS_INDEX, + name: 'rule-name', + createdBy: 'elastic', + updatedBy: 'elastic', + interval: '5m', + enabled: true, + tags: ['some fake tag 1', 'some fake tag 2'], + }); + expect(mockLogger.error).toHaveBeenCalled(); + expect(successfulsingleBulkCreate).toEqual(true); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.ts new file mode 100644 index 0000000000000..40b2eeab938dc --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.ts @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { performance } from 'perf_hooks'; +import { AlertServices } from '../../../../../alerting/server/types'; +import { SignalSearchResponse, BulkResponse } from './types'; +import { RuleTypeParams } from '../types'; +import { generateId } from './utils'; +import { buildBulkBody } from './build_bulk_body'; +import { Logger } from '../../../../../../../../src/core/server'; + +interface SingleBulkCreateParams { + someResult: SignalSearchResponse; + ruleParams: RuleTypeParams; + services: AlertServices; + logger: Logger; + id: string; + signalsIndex: string; + name: string; + createdBy: string; + updatedBy: string; + interval: string; + enabled: boolean; + tags: string[]; +} + +// Bulk Index documents. +export const singleBulkCreate = async ({ + someResult, + ruleParams, + services, + logger, + id, + signalsIndex, + name, + createdBy, + updatedBy, + interval, + enabled, + tags, +}: SingleBulkCreateParams): Promise => { + if (someResult.hits.hits.length === 0) { + return true; + } + // index documents after creating an ID based on the + // source documents' originating index, and the original + // document _id. This will allow two documents from two + // different indexes with the same ID to be + // indexed, and prevents us from creating any updates + // to the documents once inserted into the signals index, + // while preventing duplicates from being added to the + // signals index if rules are re-run over the same time + // span. Also allow for versioning. + const bulkBody = someResult.hits.hits.flatMap(doc => [ + { + create: { + _index: signalsIndex, + _id: generateId( + doc._index, + doc._id, + doc._version ? doc._version.toString() : '', + ruleParams.ruleId ?? '' + ), + }, + }, + buildBulkBody({ doc, ruleParams, id, name, createdBy, updatedBy, interval, enabled, tags }), + ]); + const time1 = performance.now(); + const firstResult: BulkResponse = await services.callCluster('bulk', { + index: signalsIndex, + refresh: false, + body: bulkBody, + }); + const time2 = performance.now(); + logger.debug( + `individual bulk process time took: ${Number(time2 - time1).toFixed(2)} milliseconds` + ); + logger.debug(`took property says bulk took: ${firstResult.took} milliseconds`); + if (firstResult.errors) { + // go through the response status errors and see what + // types of errors they are, count them up, and log them. + const errorCountMap = firstResult.items.reduce((acc: { [key: string]: number }, item) => { + if (item.create.error) { + const responseStatusKey = item.create.status.toString(); + acc[responseStatusKey] = acc[responseStatusKey] ? acc[responseStatusKey] + 1 : 1; + } + return acc; + }, {}); + /* + the logging output below should look like + {'409': 55} + which is read as "there were 55 counts of 409 errors returned from bulk create" + */ + logger.error( + `[-] bulkResponse had errors with response statuses:counts of...\n${JSON.stringify( + errorCountMap, + null, + 2 + )}` + ); + } + return true; +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_search_after.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_search_after.test.ts new file mode 100644 index 0000000000000..a5d1f66d3089e --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_search_after.test.ts @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { savedObjectsClientMock } from 'src/core/server/mocks'; +import { + sampleRuleAlertParams, + sampleDocSearchResultsNoSortId, + mockLogger, + sampleDocSearchResultsWithSortId, +} from './__mocks__/es_results'; +import { singleSearchAfter } from './single_search_after'; + +export const mockService = { + callCluster: jest.fn(), + alertInstanceFactory: jest.fn(), + savedObjectsClient: savedObjectsClientMock.create(), +}; + +describe('singleSearchAfter', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('if singleSearchAfter works without a given sort id', async () => { + let searchAfterSortId; + const sampleParams = sampleRuleAlertParams(); + mockService.callCluster.mockReturnValue(sampleDocSearchResultsNoSortId); + await expect( + singleSearchAfter({ + searchAfterSortId, + ruleParams: sampleParams, + services: mockService, + logger: mockLogger, + pageSize: 1, + filter: undefined, + }) + ).rejects.toThrow('Attempted to search after with empty sort id'); + }); + test('if singleSearchAfter works with a given sort id', async () => { + const searchAfterSortId = '1234567891111'; + const sampleParams = sampleRuleAlertParams(); + mockService.callCluster.mockReturnValue(sampleDocSearchResultsWithSortId); + const searchAfterResult = await singleSearchAfter({ + searchAfterSortId, + ruleParams: sampleParams, + services: mockService, + logger: mockLogger, + pageSize: 1, + filter: undefined, + }); + expect(searchAfterResult).toEqual(sampleDocSearchResultsWithSortId); + }); + test('if singleSearchAfter throws error', async () => { + const searchAfterSortId = '1234567891111'; + const sampleParams = sampleRuleAlertParams(); + mockService.callCluster.mockImplementation(async () => { + throw Error('Fake Error'); + }); + await expect( + singleSearchAfter({ + searchAfterSortId, + ruleParams: sampleParams, + services: mockService, + logger: mockLogger, + pageSize: 1, + filter: undefined, + }) + ).rejects.toThrow('Fake Error'); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_search_after.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_search_after.ts new file mode 100644 index 0000000000000..3a99500cb3433 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_search_after.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { RuleTypeParams } from '../types'; +import { AlertServices } from '../../../../../alerting/server/types'; +import { Logger } from '../../../../../../../../src/core/server'; +import { SignalSearchResponse } from './types'; +import { buildEventsSearchQuery } from './build_events_query'; + +interface SingleSearchAfterParams { + searchAfterSortId: string | undefined; + ruleParams: RuleTypeParams; + services: AlertServices; + logger: Logger; + pageSize: number; + filter: unknown; +} + +// utilize search_after for paging results into bulk. +export const singleSearchAfter = async ({ + searchAfterSortId, + ruleParams, + services, + filter, + logger, + pageSize, +}: SingleSearchAfterParams): Promise => { + if (searchAfterSortId == null) { + throw Error('Attempted to search after with empty sort id'); + } + try { + const searchAfterQuery = buildEventsSearchQuery({ + index: ruleParams.index, + from: ruleParams.from, + to: ruleParams.to, + filter, + size: pageSize, + searchAfterSortId, + }); + const nextSearchAfterResult: SignalSearchResponse = await services.callCluster( + 'search', + searchAfterQuery + ); + return nextSearchAfterResult; + } catch (exc) { + logger.error(`[-] nextSearchAfter threw an error ${exc}`); + throw exc; + } +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/types.ts new file mode 100644 index 0000000000000..213ceb29a6e25 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/types.ts @@ -0,0 +1,123 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { RuleAlertParams, OutputRuleAlertRest } from '../types'; +import { SearchResponse } from '../../types'; +import { RequestFacade } from '../../../types'; +import { AlertType, State, AlertExecutorOptions } from '../../../../../alerting/server/types'; + +export interface SignalsParams { + signalIds: string[] | undefined | null; + query: object | undefined | null; + status: 'open' | 'closed'; +} + +export type SignalsRestParams = Omit & { + signal_ids: SignalsParams['signalIds']; +}; + +export interface SignalsRequest extends RequestFacade { + payload: SignalsRestParams; +} + +export type SearchTypes = + | string + | string[] + | number + | number[] + | boolean + | boolean[] + | object + | object[]; + +export interface SignalSource { + [key: string]: SearchTypes; + '@timestamp': string; +} + +export interface BulkResponse { + took: number; + errors: boolean; + items: [ + { + create: { + _index: string; + _type?: string; + _id: string; + _version: number; + result?: string; + _shards?: { + total: number; + successful: number; + failed: number; + }; + _seq_no?: number; + _primary_term?: number; + status: number; + error?: { + type: string; + reason: string; + index_uuid?: string; + shard: string; + index: string; + }; + }; + } + ]; +} + +export interface MGetResponse { + docs: GetResponse[]; +} +export interface GetResponse { + _index: string; + _type: string; + _id: string; + _version: number; + _seq_no: number; + _primary_term: number; + found: boolean; + _source: SearchTypes; +} + +export type SignalSearchResponse = SearchResponse; +export type SignalSourceHit = SignalSearchResponse['hits']['hits'][0]; + +export type RuleExecutorOptions = Omit & { + params: RuleAlertParams & { + scrollSize: number; + scrollLock: string; + }; +}; + +// This returns true because by default a RuleAlertTypeDefinition is an AlertType +// since we are only increasing the strictness of params. +export const isAlertExecutor = (obj: SignalRuleAlertTypeDefinition): obj is AlertType => { + return true; +}; + +export type SignalRuleAlertTypeDefinition = Omit & { + executor: ({ services, params, state }: RuleExecutorOptions) => Promise; +}; + +export interface Signal { + rule: Partial; + parent: { + id: string; + type: string; + index: string; + depth: number; + }; + original_time: string; + original_event?: SearchTypes; + status: 'open' | 'closed'; +} + +export interface SignalHit { + '@timestamp': string; + event: object; + signal: Partial; +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/utils.ts new file mode 100644 index 0000000000000..f25ce1d905466 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/utils.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { createHash } from 'crypto'; + +export const generateId = ( + docIndex: string, + docId: string, + version: string, + ruleId: string +): string => + createHash('sha256') + .update(docIndex.concat(docId, version, ruleId)) + .digest('hex'); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts new file mode 100644 index 0000000000000..d02595c368aa7 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { esFilters } from '../../../../../../../src/plugins/data/server'; + +export type PartialFilter = Partial; + +export interface IMitreAttack { + id: string; + name: string; + reference: string; +} + +export interface ThreatParams { + framework: string; + tactic: IMitreAttack; + techniques: IMitreAttack[]; +} + +export interface RuleAlertParams { + description: string; + enabled: boolean; + falsePositives: string[]; + filters: PartialFilter[] | undefined | null; + from: string; + immutable: boolean; + index: string[]; + interval: string; + ruleId: string | undefined | null; + language: string | undefined | null; + maxSignals: number; + riskScore: number; + outputIndex: string; + name: string; + query: string | undefined | null; + references: string[]; + savedId: string | undefined | null; + meta: Record | undefined | null; + severity: string; + tags: string[]; + to: string; + threats: ThreatParams[] | undefined | null; + type: 'query' | 'saved_query'; +} + +export type RuleTypeParams = Omit; + +export type RuleAlertParamsRest = Omit< + RuleAlertParams, + 'ruleId' | 'falsePositives' | 'maxSignals' | 'savedId' | 'riskScore' | 'outputIndex' +> & { + rule_id: RuleAlertParams['ruleId']; + false_positives: RuleAlertParams['falsePositives']; + saved_id: RuleAlertParams['savedId']; + max_signals: RuleAlertParams['maxSignals']; + risk_score: RuleAlertParams['riskScore']; + output_index: RuleAlertParams['outputIndex']; +}; + +export type OutputRuleAlertRest = RuleAlertParamsRest & { + id: string; + created_by: string | undefined | null; + updated_by: string | undefined | null; +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/types.ts b/x-pack/legacy/plugins/siem/server/lib/types.ts index e97a07e276dcf..9e4e477aa78d2 100644 --- a/x-pack/legacy/plugins/siem/server/lib/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/types.ts @@ -23,7 +23,6 @@ import { Note } from './note/saved_object'; import { PinnedEvent } from './pinned_event/saved_object'; import { Timeline } from './timeline/saved_object'; import { TLS } from './tls'; -import { SearchTypes, OutputRuleAlertRest } from './detection_engine/alerts/types'; export * from './hosts'; @@ -55,25 +54,6 @@ export interface SiemContext { req: FrameworkRequest; } -export interface Signal { - rule: Partial; - parent: { - id: string; - type: string; - index: string; - depth: number; - }; - original_time: string; - original_event?: SearchTypes; - status: 'open' | 'closed'; -} - -export interface SignalHit { - '@timestamp': string; - event: object; - signal: Partial; -} - export interface TotalValue { value: number; relation: string; From 8115e500ff9daf8172d02baca533a48787b9dcfa Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Tue, 10 Dec 2019 10:46:56 -0500 Subject: [PATCH 03/40] [SIEM] [DETECTION ENG] Add MITRE ATT&CK (#52398) * add mitre attack enterprise * Add Mitre Att&ck on the about rule * review * fix internatiolazition * bugs review * fix ux with add reference --- x-pack/legacy/plugins/siem/package.json | 1 + .../components/add_item_form/index.tsx | 29 +- .../components/add_item_form/translations.ts | 14 - .../components/description_step/index.tsx | 124 +- .../create_rule/components/mitre/index.tsx | 171 + .../components/mitre/translations.ts | 36 + .../components/query_bar/index.tsx | 59 +- .../step_about_rule/default_value.ts | 7 + .../components/step_about_rule/index.tsx | 11 + .../components/step_about_rule/schema.tsx | 44 +- .../step_about_rule/translations.ts | 7 + .../components/step_define_rule/index.tsx | 267 +- .../components/step_define_rule/schema.tsx | 21 - .../detection_engine/create_rule/helpers.ts | 15 +- .../detection_engine/create_rule/index.tsx | 15 +- .../create_rule/translations.ts | 4 + .../detection_engine/create_rule/types.ts | 16 +- .../mitre/mitre_tactics_techniques.ts | 4696 +++++++++++++++++ .../pages/detection_engine/mitre/types.ts | 21 + .../extract_tactics_techniques_mitre.js | 113 + 20 files changed, 5441 insertions(+), 230 deletions(-) delete mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/add_item_form/translations.ts create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/mitre/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/mitre/translations.ts create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/mitre/mitre_tactics_techniques.ts create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/mitre/types.ts create mode 100644 x-pack/legacy/plugins/siem/scripts/extract_tactics_techniques_mitre.js diff --git a/x-pack/legacy/plugins/siem/package.json b/x-pack/legacy/plugins/siem/package.json index d239961ee75d7..ef6431327b5ab 100644 --- a/x-pack/legacy/plugins/siem/package.json +++ b/x-pack/legacy/plugins/siem/package.json @@ -5,6 +5,7 @@ "private": true, "license": "Elastic-License", "scripts": { + "extract-mitre-attacks": "node scripts/extract_tactics_techniques_mitre.js & node ../../../../scripts/eslint ./public/pages/detection_engine/mitre/mitre_tactics_techniques.ts --fix", "build-graphql-types": "node scripts/generate_types_from_graphql.js", "cypress:open": "../../../node_modules/.bin/cypress open", "cypress:run": "../../../node_modules/.bin/cypress run --spec ./cypress/integration/**/*.spec.ts --reporter ../../../node_modules/cypress-multi-reporters --reporter-options configFile=./reporter_config.json; status=$?; ../../../node_modules/.bin/mochawesome-merge --reportDir ../../../../target/kibana-siem/cypress/results > ../../../../target/kibana-siem/cypress/results/output.json; ../../../../node_modules/.bin/marge ../../../../target/kibana-siem/cypress/results/output.json --reportDir ../../../../target/kibana-siem/cypress/results; mkdir -p ../../../../target/junit && cp ../../../../target/kibana-siem/cypress/results/*.xml ../../../../target/junit/ && exit $status;" diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/add_item_form/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/add_item_form/index.tsx index 04bca0cdbd61b..e972cd21b6be9 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/add_item_form/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/add_item_form/index.tsx @@ -9,7 +9,7 @@ import { isEmpty } from 'lodash/fp'; import React, { ChangeEvent, useCallback, useEffect, useState, useRef } from 'react'; import { FieldHook, getFieldValidityAndErrorMessage } from '../shared_imports'; -import * as I18n from './translations'; +import * as CreateRuleI18n from '../../translations'; interface AddItemProps { addText: string; @@ -34,18 +34,21 @@ export const AddItem = ({ addText, dataTestSubj, field, idAria, isDisabled }: Ad ...inputsRef.current.slice(0, index), ...inputsRef.current.slice(index + 1), ]; - if (inputsRef.current[index] != null) { - inputsRef.current[index].value = 're-render'; - } + inputsRef.current = inputsRef.current.map((ref, i) => { + if (i >= index && inputsRef.current[index] != null) { + ref.value = 're-render'; + } + return ref; + }); }, [field] ); const addItem = useCallback(() => { const values = field.value as string[]; - if (!isEmpty(values[values.length - 1])) { + if (!isEmpty(values) && values[values.length - 1]) { field.setValue([...values, '']); - } else { + } else if (isEmpty(values)) { field.setValue(['']); } }, [field]); @@ -62,9 +65,12 @@ export const AddItem = ({ addText, dataTestSubj, field, idAria, isDisabled }: Ad ...inputsRef.current.slice(index + 1), ]; setHaveBeenKeyboardDeleted(inputsRef.current.length - 1); - if (inputsRef.current[index] != null) { - inputsRef.current[index].value = 're-render'; - } + inputsRef.current = inputsRef.current.map((ref, i) => { + if (i >= index && inputsRef.current[index] != null) { + ref.value = 're-render'; + } + return ref; + }); } else { field.setValue([...values.slice(0, index), value, ...values.slice(index + 1)]); } @@ -114,7 +120,8 @@ export const AddItem = ({ addText, dataTestSubj, field, idAria, isDisabled }: Ad ...(index === values.length - 1 ? { inputRef: handleLastInputRef.bind(null, index) } : {}), - ...(inputsRef.current[index] != null && inputsRef.current[index].value !== item + ...((inputsRef.current[index] != null && inputsRef.current[index].value !== item) || + inputsRef.current[index] == null ? { value: item } : {}), }; @@ -127,7 +134,7 @@ export const AddItem = ({ addText, dataTestSubj, field, idAria, isDisabled }: Ad iconType="trash" isDisabled={isDisabled} onClick={() => removeItem(index)} - aria-label={I18n.DELETE} + aria-label={CreateRuleI18n.DELETE} /> } onChange={e => updateItem(e, index)} diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/add_item_form/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/add_item_form/translations.ts deleted file mode 100644 index 98c15606d88fe..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/add_item_form/translations.ts +++ /dev/null @@ -1,14 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; - -export const DELETE = i18n.translate( - 'xpack.siem.detectionEngine.createRule.addItem.deleteDescription', - { - defaultMessage: 'Delete', - } -); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/description_step/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/description_step/index.tsx index 3e8147e5ca3c1..29e1bc228e066 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/description_step/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/description_step/index.tsx @@ -4,7 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiBadge, EuiDescriptionList, EuiFlexGroup, EuiFlexItem, EuiTextArea } from '@elastic/eui'; +import { + EuiBadge, + EuiDescriptionList, + EuiFlexGroup, + EuiFlexItem, + EuiTextArea, + EuiLink, + EuiText, + EuiListGroup, +} from '@elastic/eui'; import { isEmpty, chunk, get, pick } from 'lodash/fp'; import React, { memo, ReactNode } from 'react'; import styled from 'styled-components'; @@ -20,6 +29,9 @@ import { FilterLabel } from './filter_label'; import { FormSchema } from '../shared_imports'; import * as I18n from './translations'; +import { IMitreEnterpriseAttack } from '../../types'; +import { tacticsOptions, techniquesOptions } from '../../../mitre/mitre_tactics_techniques'; + interface StepRuleDescriptionProps { data: unknown; indexPatterns?: IIndexPattern; @@ -36,16 +48,34 @@ const EuiFlexItemWidth = styled(EuiFlexItem)` width: 50%; `; +const MyEuiListGroup = styled(EuiListGroup)` + padding: 0px; + .euiListGroupItem__button { + padding: 0px; + } +`; + +const ThreatsEuiFlexGroup = styled(EuiFlexGroup)` + .euiFlexItem { + margin-bottom: 0px; + } +`; + export const StepRuleDescription = memo( ({ data, indexPatterns, schema }) => { const keys = Object.keys(schema); + const listItems = keys.reduce( + (acc: ListItems[], key: string) => [ + ...acc, + ...buildListItems(data, pick(key, schema), indexPatterns), + ], + [] + ); return ( - {chunk(keys.includes('queryBar') ? 3 : Math.ceil(keys.length / 2), keys).map(key => ( - - + {chunk(Math.ceil(listItems.length / 2), listItems).map((chunckListItems, index) => ( + + ))} @@ -77,7 +107,9 @@ const getDescriptionItem = ( value: unknown, indexPatterns?: IIndexPattern ): ListItems[] => { - if (field === 'queryBar' && indexPatterns != null) { + if (field === 'useIndicesConfig') { + return []; + } else if (field === 'queryBar' && indexPatterns != null) { const filters = get('queryBar.filters', value) as esFilters.Filter[]; const query = get('queryBar.query', value) as Query; const savedId = get('queryBar.saved_id', value); @@ -123,6 +155,50 @@ const getDescriptionItem = ( ]; } return items; + } else if (field === 'threats') { + const threats: IMitreEnterpriseAttack[] = get(field, value).filter( + (threat: IMitreEnterpriseAttack) => threat.tactic.name !== 'none' + ); + if (threats.length > 0) { + return [ + { + title: label, + description: ( + + {threats.map((threat, index) => { + const tactic = tacticsOptions.find(t => t.name === threat.tactic.name); + return ( + + +
+ + {tactic != null ? tactic.text : ''} + +
+ { + const myTechnique = techniquesOptions.find( + t => t.name === technique.name + ); + return { + label: myTechnique != null ? myTechnique.label : '', + href: technique.reference, + target: '_blank', + }; + })} + /> +
+
+ ); + })} +
+ ), + }, + ]; + } + return []; } else if (field === 'description') { return [ { @@ -131,20 +207,26 @@ const getDescriptionItem = ( }, ]; } else if (Array.isArray(get(field, value))) { - return [ - { - title: label, - description: ( - - {get(field, value).map((val: string) => ( - - {val} - - ))} - - ), - }, - ]; + const values: string[] = get(field, value); + if (!isEmpty(values) && values.filter(val => !isEmpty(val)).length > 0) { + return [ + { + title: label, + description: ( + + {values.map((val: string) => + isEmpty(val) ? null : ( + + {val} + + ) + )} + + ), + }, + ]; + } + return []; } return [ { diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/mitre/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/mitre/index.tsx new file mode 100644 index 0000000000000..6ab4ca4b51447 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/mitre/index.tsx @@ -0,0 +1,171 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + EuiButtonEmpty, + EuiButtonIcon, + EuiFormRow, + EuiSelect, + EuiSpacer, + EuiFlexGroup, + EuiFlexItem, + EuiComboBox, + EuiFormControlLayout, +} from '@elastic/eui'; +import { isEmpty, kebabCase, camelCase } from 'lodash/fp'; +import React, { ChangeEvent, useCallback } from 'react'; +import styled from 'styled-components'; + +import { tacticsOptions, techniquesOptions } from '../../../mitre/mitre_tactics_techniques'; +import * as CreateRuleI18n from '../../translations'; +import { FieldHook, getFieldValidityAndErrorMessage } from '../shared_imports'; +import * as I18n from './translations'; +import { IMitreEnterpriseAttack } from '../../types'; + +const MyEuiFormControlLayout = styled(EuiFormControlLayout)` + &.euiFormControlLayout--compressed { + height: fit-content !important; + } +`; +interface AddItemProps { + field: FieldHook; + dataTestSubj: string; + idAria: string; + isDisabled: boolean; +} + +export const AddMitreThreat = ({ dataTestSubj, field, idAria, isDisabled }: AddItemProps) => { + const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); + + const removeItem = useCallback( + (index: number) => { + const values = field.value as string[]; + field.setValue([...values.slice(0, index), ...values.slice(index + 1)]); + }, + [field] + ); + + const addItem = useCallback(() => { + const values = field.value as IMitreEnterpriseAttack[]; + if (!isEmpty(values[values.length - 1])) { + field.setValue([ + ...values, + { tactic: { id: 'none', name: 'none', reference: 'none' }, techniques: [] }, + ]); + } else { + field.setValue([{ tactic: { id: 'none', name: 'none', reference: 'none' }, techniques: [] }]); + } + }, [field]); + + const updateTactic = useCallback( + (index: number, event: ChangeEvent) => { + const values = field.value as IMitreEnterpriseAttack[]; + const { id, reference, name } = tacticsOptions.find(t => t.value === event.target.value) || { + id: '', + name: '', + reference: '', + }; + field.setValue([ + ...values.slice(0, index), + { + ...values[index], + tactic: { id, reference, name }, + techniques: [], + }, + ...values.slice(index + 1), + ]); + }, + [field] + ); + + const updateTechniques = useCallback( + (index: number, selectedOptions: unknown[]) => { + field.setValue([ + ...values.slice(0, index), + { + ...values[index], + techniques: selectedOptions, + }, + ...values.slice(index + 1), + ]); + }, + [field] + ); + + const values = field.value as IMitreEnterpriseAttack[]; + + return ( + + <> + {values.map((item, index) => { + const euiSelectFieldProps = { + disabled: isDisabled, + }; + return ( +
+ + + ({ text: t.text, value: t.value })), + ]} + aria-label="" + onChange={updateTactic.bind(null, index)} + prepend={I18n.TACTIC} + compressed + fullWidth={false} + value={camelCase(item.tactic.name)} + {...euiSelectFieldProps} + /> + + + + + t.tactics.includes(kebabCase(item.tactic.name)) + )} + selectedOptions={item.techniques} + onChange={updateTechniques.bind(null, index)} + isDisabled={isDisabled} + fullWidth={true} + /> + + + + removeItem(index)} + aria-label={CreateRuleI18n.DELETE} + /> + + + {values.length - 1 !== index && } +
+ ); + })} + + {I18n.ADD_MITRE_ATTACK} + + +
+ ); +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/mitre/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/mitre/translations.ts new file mode 100644 index 0000000000000..22ee6cc3ef911 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/mitre/translations.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const TACTIC = i18n.translate('xpack.siem.detectionEngine.mitreAttack.tacticsDescription', { + defaultMessage: 'Tactic', +}); + +export const TECHNIQUES = i18n.translate( + 'xpack.siem.detectionEngine.mitreAttack.techniquesDescription', + { + defaultMessage: 'Techniques', + } +); + +export const ADD_MITRE_ATTACK = i18n.translate('xpack.siem.detectionEngine.mitreAttack.addTitle', { + defaultMessage: 'Add MITRE ATT&CK threat', +}); + +export const TECHNIQUES_PLACEHOLDER = i18n.translate( + 'xpack.siem.detectionEngine.mitreAttack.techniquesPlaceHolderDescription', + { + defaultMessage: 'Select techniques ...', + } +); + +export const TACTIC_PLACEHOLDER = i18n.translate( + 'xpack.siem.detectionEngine.mitreAttack.tacticPlaceHolderDescription', + { + defaultMessage: 'Select tactic ...', + } +); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/query_bar/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/query_bar/index.tsx index 92b2f557d4cec..8dc402f00e621 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/query_bar/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/query_bar/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiFormRow } from '@elastic/eui'; +import { EuiFormRow, EuiMutationObserver } from '@elastic/eui'; import { isEqual } from 'lodash/fp'; import React, { useCallback, useEffect, useState } from 'react'; import { Subscription } from 'rxjs'; @@ -36,6 +36,7 @@ interface QueryBarDefineRuleProps { idAria: string; isLoading: boolean; indexPattern: IIndexPattern; + resizeParentContainer?: (height: number) => void; } const StyledEuiFormRow = styled(EuiFormRow)` @@ -60,7 +61,9 @@ export const QueryBarDefineRule = ({ idAria, indexPattern, isLoading = false, + resizeParentContainer, }: QueryBarDefineRuleProps) => { + const [originalHeight, setOriginalHeight] = useState(-1); const [savedQuery, setSavedQuery] = useState(null); const [queryDraft, setQueryDraft] = useState({ query: '', language: 'kuery' }); const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); @@ -165,6 +168,27 @@ export const QueryBarDefineRule = ({ [field.value] ); + const onMutation = (event: unknown, observer: unknown) => { + if (resizeParentContainer != null) { + const suggestionContainer = document.getElementById('kbnTypeahead__items'); + if (suggestionContainer != null) { + const box = suggestionContainer.getBoundingClientRect(); + const accordionContainer = document.getElementById('define-rule'); + if (accordionContainer != null) { + const accordionBox = accordionContainer.getBoundingClientRect(); + if (originalHeight === -1 || accordionBox.height < originalHeight + box.height) { + resizeParentContainer(originalHeight + box.height - 100); + } + if (originalHeight === -1) { + setOriginalHeight(accordionBox.height); + } + } + } else { + resizeParentContainer(-1); + } + } + }; + return ( - + + {mutationRef => ( +
+ +
+ )} +
); }; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/default_value.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/default_value.ts index 7c4d78f364479..504b5ca85a3ab 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/default_value.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/default_value.ts @@ -15,4 +15,11 @@ export const defaultValue: AboutStepRule = { references: [''], falsePositives: [''], tags: [], + threats: [ + { + framework: 'MITRE ATT&CK', + tactic: { id: 'none', name: 'none', reference: 'none' }, + techniques: [], + }, + ], }; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/index.tsx index 56830f252748f..aeb70061c44bf 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/index.tsx @@ -16,6 +16,7 @@ import { defaultValue } from './default_value'; import { schema } from './schema'; import * as I18n from './translations'; import { StepRuleDescription } from '../description_step'; +import { AddMitreThreat } from '../mitre'; const CommonUseField = getUseField({ component: Field }); @@ -114,6 +115,16 @@ export const StepAboutRule = memo(({ isEditView, isLoading, setSt dataTestSubj: 'detectionEngineStepAboutRuleFalsePositives', }} /> + {CreateRuleI18n.OPTIONAL_FIELD}, }, + threats: { + label: i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepAboutRule.fieldMitreThreatLabel', + { + defaultMessage: 'MITRE ATT&CK', + } + ), + labelAppend: {CreateRuleI18n.OPTIONAL_FIELD}, + validations: [ + { + validator: ( + ...args: Parameters + ): ReturnType> | undefined => { + const [{ value, path }] = args; + let hasError = false; + (value as IMitreEnterpriseAttack[]).forEach(v => { + if (isEmpty(v.tactic.name) || (v.tactic.name !== 'none' && isEmpty(v.techniques))) { + hasError = true; + } + }); + return hasError + ? { + code: 'ERR_FIELD_MISSING', + path, + message: I18n.CUSTOM_MITRE_ATTACK_TECHNIQUES_REQUIRED, + } + : undefined; + }, + }, + ], + }, tags: { type: FIELD_TYPES.COMBO_BOX, label: i18n.translate('xpack.siem.detectionEngine.createRule.stepAboutRule.fieldTagsLabel', { diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/translations.ts index bd759b345d70d..017d4fe6fdf49 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_about_rule/translations.ts @@ -47,3 +47,10 @@ export const CRITICAL = i18n.translate( defaultMessage: 'Critical', } ); + +export const CUSTOM_MITRE_ATTACK_TECHNIQUES_REQUIRED = i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepDefineRule.customMitreAttackTechniquesFieldRequiredError', + { + defaultMessage: 'At least one Technique is required with a Tactic.', + } +); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_define_rule/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_define_rule/index.tsx index 26306d3573926..6954bd6bf733f 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_define_rule/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_define_rule/index.tsx @@ -6,11 +6,11 @@ import { EuiHorizontalRule, EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui'; import { isEqual } from 'lodash/fp'; -import React, { memo, useCallback, useEffect, useState } from 'react'; +import React, { memo, useCallback, useState } from 'react'; import { IIndexPattern } from '../../../../../../../../../../src/plugins/data/public'; import { useFetchIndexPatterns } from '../../../../../containers/detection_engine/rules/fetch_index_patterns'; -import { DEFAULT_INDEX_KEY, DEFAULT_SIGNALS_INDEX_KEY } from '../../../../../../common/constants'; +import { DEFAULT_INDEX_KEY } from '../../../../../../common/constants'; import { useKibanaUiSetting } from '../../../../../lib/settings/use_kibana_ui_setting'; import * as CreateRuleI18n from '../../translations'; import { DefineStepRule, RuleStep, RuleStepProps } from '../../types'; @@ -22,148 +22,133 @@ import * as I18n from './translations'; const CommonUseField = getUseField({ component: Field }); -export const StepDefineRule = memo(({ isEditView, isLoading, setStepData }) => { - const [initializeOutputIndex, setInitializeOutputIndex] = useState(true); - const [localUseIndicesConfig, setLocalUseIndicesConfig] = useState(''); - const [ - { indexPatterns: indexPatternQueryBar, isLoading: indexPatternLoadingQueryBar }, - setIndices, - ] = useFetchIndexPatterns(); - const [indicesConfig] = useKibanaUiSetting(DEFAULT_INDEX_KEY); - const [signalIndexConfig] = useKibanaUiSetting(DEFAULT_SIGNALS_INDEX_KEY); - const [myStepData, setMyStepData] = useState({ - index: indicesConfig || [], - isNew: true, - outputIndex: signalIndexConfig, - queryBar: { - query: { query: '', language: 'kuery' }, - filters: [], - saved_id: null, - }, - useIndicesConfig: 'true', - }); - const { form } = useForm({ - schema, - defaultValue: myStepData, - options: { stripEmptyFields: false }, - }); +export const StepDefineRule = memo( + ({ isEditView, isLoading, resizeParentContainer, setStepData }) => { + const [localUseIndicesConfig, setLocalUseIndicesConfig] = useState(''); + const [ + { indexPatterns: indexPatternQueryBar, isLoading: indexPatternLoadingQueryBar }, + setIndices, + ] = useFetchIndexPatterns(); + const [indicesConfig] = useKibanaUiSetting(DEFAULT_INDEX_KEY); + const [myStepData, setMyStepData] = useState({ + index: indicesConfig || [], + isNew: true, + queryBar: { + query: { query: '', language: 'kuery' }, + filters: [], + saved_id: null, + }, + useIndicesConfig: 'true', + }); + const { form } = useForm({ + schema, + defaultValue: myStepData, + options: { stripEmptyFields: false }, + }); - const onSubmit = useCallback(async () => { - const { isValid, data } = await form.submit(); - if (isValid) { - setStepData(RuleStep.defineRule, data, isValid); - setMyStepData({ ...data, isNew: false } as DefineStepRule); - } - }, [form]); + const onSubmit = useCallback(async () => { + const { isValid, data } = await form.submit(); + if (isValid) { + setStepData(RuleStep.defineRule, data, isValid); + setMyStepData({ ...data, isNew: false } as DefineStepRule); + } + }, [form]); - useEffect(() => { - if (signalIndexConfig != null && initializeOutputIndex) { - const outputIndexField = form.getFields().outputIndex; - outputIndexField.setValue(signalIndexConfig); - setInitializeOutputIndex(false); - } - }, [initializeOutputIndex, signalIndexConfig, form]); - - return isEditView && myStepData != null ? ( - - ) : ( - <> -
- - + ) : ( + <> + + + + - - - - {({ useIndicesConfig }) => { - if (localUseIndicesConfig !== useIndicesConfig) { - const indexField = form.getFields().index; - if ( - indexField != null && - useIndicesConfig === 'true' && - !isEqual(indexField.value, indicesConfig) - ) { - indexField.setValue(indicesConfig); - setIndices(indicesConfig); - } else if ( - indexField != null && - useIndicesConfig === 'false' && - isEqual(indexField.value, indicesConfig) - ) { - indexField.setValue([]); - setIndices([]); + isLoading: indexPatternLoadingQueryBar, + dataTestSubj: 'detectionEngineStepDefineRuleQueryBar', + resizeParentContainer, + }} + /> + + {({ useIndicesConfig }) => { + if (localUseIndicesConfig !== useIndicesConfig) { + const indexField = form.getFields().index; + if ( + indexField != null && + useIndicesConfig === 'true' && + !isEqual(indexField.value, indicesConfig) + ) { + indexField.setValue(indicesConfig); + setIndices(indicesConfig); + } else if ( + indexField != null && + useIndicesConfig === 'false' && + isEqual(indexField.value, indicesConfig) + ) { + indexField.setValue([]); + setIndices([]); + } + setLocalUseIndicesConfig(useIndicesConfig); } - setLocalUseIndicesConfig(useIndicesConfig); - } - return null; - }} - - - - - - - {myStepData.isNew ? CreateRuleI18n.CONTINUE : CreateRuleI18n.UPDATE} - - - - - ); -}); + return null; + }} + + + + + + + {myStepData.isNew ? CreateRuleI18n.CONTINUE : CreateRuleI18n.UPDATE} + + + + + ); + } +); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_define_rule/schema.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_define_rule/schema.tsx index 9f1644e73bf0b..0f6c5f72e1683 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_define_rule/schema.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/step_define_rule/schema.tsx @@ -26,27 +26,6 @@ import { CUSTOM_QUERY_REQUIRED, INVALID_CUSTOM_QUERY } from './translations'; const { emptyField } = fieldValidators; export const schema: FormSchema = { - outputIndex: { - type: FIELD_TYPES.TEXT, - label: i18n.translate( - 'xpack.siem.detectionEngine.createRule.stepDefineRule.fieldOutputIndiceNameLabel', - { - defaultMessage: 'Output index name', - } - ), - validations: [ - { - validator: emptyField( - i18n.translate( - 'xpack.siem.detectionEngine.createRule.stepDefineRule.outputIndiceNameFieldRequiredError', - { - defaultMessage: 'An output indice name for signals is required.', - } - ) - ), - }, - ], - }, useIndicesConfig: { type: FIELD_TYPES.RADIO_GROUP, label: i18n.translate( diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/helpers.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/helpers.ts index b864260dd3338..f6546a680ad81 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/helpers.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/helpers.ts @@ -40,13 +40,12 @@ const getTimeTypeValue = (time: string): { unit: string; value: number } => { }; const formatDefineStepData = (defineStepData: DefineStepRule): DefineStepRuleJson => { - const { queryBar, useIndicesConfig, outputIndex, ...rest } = defineStepData; + const { queryBar, useIndicesConfig, ...rest } = defineStepData; const { filters, query, saved_id: savedId } = queryBar; return { ...rest, language: query.language, filters, - output_index: outputIndex, query: query.query as string, ...(savedId != null ? { saved_id: savedId } : {}), }; @@ -69,12 +68,22 @@ const formatScheduleStepData = (scheduleData: ScheduleStepRule): ScheduleStepRul }; const formatAboutStepData = (aboutStepData: AboutStepRule): AboutStepRuleJson => { - const { falsePositives, references, riskScore, ...rest } = aboutStepData; + const { falsePositives, references, riskScore, threats, ...rest } = aboutStepData; return { false_positives: falsePositives.filter(item => !isEmpty(item)), references: references.filter(item => !isEmpty(item)), risk_score: riskScore, + threats: threats + .filter(threat => threat.tactic.name !== 'none') + .map(threat => ({ + ...threat, + framework: 'MITRE ATT&CK', + techniques: threat.techniques.map(technique => { + const { id, name, reference } = technique; + return { id, name, reference }; + }), + })), ...rest, }; }; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/index.tsx index 878c7171d19ed..393b72d16b0a4 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/index.tsx @@ -7,6 +7,7 @@ import { EuiButtonEmpty, EuiAccordion, EuiHorizontalRule, EuiPanel, EuiSpacer } from '@elastic/eui'; import React, { useCallback, useRef, useState } from 'react'; import { Redirect } from 'react-router-dom'; +import styled from 'styled-components'; import { HeaderPage } from '../../../components/header_page'; import { WrapperPage } from '../../../components/wrapper_page'; @@ -24,7 +25,16 @@ import { DETECTION_ENGINE_PAGE_NAME } from '../../../components/link_to/redirect const stepsRuleOrder = [RuleStep.defineRule, RuleStep.aboutRule, RuleStep.scheduleRule]; +const ResizeEuiPanel = styled(EuiPanel)<{ + height?: number; +}>` + .euiAccordion__childWrapper { + height: ${props => (props.height !== -1 ? `${props.height}px !important` : 'auto')}; + } +`; + export const CreateRuleComponent = React.memo(() => { + const [heightAccordion, setHeightAccordion] = useState(-1); const [openAccordionId, setOpenAccordionId] = useState(RuleStep.defineRule); const defineRuleRef = useRef(null); const aboutRuleRef = useRef(null); @@ -169,7 +179,7 @@ export const CreateRuleComponent = React.memo(() => { isLoading={isLoading} title={i18n.PAGE_TITLE} /> - + { isEditView={isStepRuleInEditView[RuleStep.defineRule]} isLoading={isLoading} setStepData={setStepData} + resizeParentContainer={height => setHeightAccordion(height)} /> - + void; isEditView: boolean; isLoading: boolean; + resizeParentContainer?: (height: number) => void; } interface StepRuleData { @@ -36,10 +37,10 @@ export interface AboutStepRule extends StepRuleData { references: string[]; falsePositives: string[]; tags: string[]; + threats: IMitreEnterpriseAttack[]; } export interface DefineStepRule extends StepRuleData { - outputIndex: string; useIndicesConfig: string; index: string[]; queryBar: FieldValueQueryBar; @@ -53,7 +54,6 @@ export interface ScheduleStepRule extends StepRuleData { } export interface DefineStepRuleJson { - output_index: string; index: string[]; filters: esFilters.Filter[]; saved_id?: string; @@ -69,8 +69,20 @@ export interface AboutStepRuleJson { references: string[]; false_positives: string[]; tags: string[]; + threats: IMitreEnterpriseAttack[]; } export type ScheduleStepRuleJson = ScheduleStepRule; export type FormatRuleType = 'query' | 'saved_query'; + +export interface IMitreAttack { + id: string; + name: string; + reference: string; +} +export interface IMitreEnterpriseAttack { + framework: string; + tactic: IMitreAttack; + techniques: IMitreAttack[]; +} diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/mitre/mitre_tactics_techniques.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/mitre/mitre_tactics_techniques.ts new file mode 100644 index 0000000000000..160e006c4d267 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/mitre/mitre_tactics_techniques.ts @@ -0,0 +1,4696 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +import { MitreTacticsOptions, MitreTechniquesOptions } from './types'; + +export const tactics = [ + { + name: 'Collection', + id: 'TA0009', + reference: 'https://attack.mitre.org/tactics/TA0009', + }, + { + name: 'Command and Control', + id: 'TA0011', + reference: 'https://attack.mitre.org/tactics/TA0011', + }, + { + name: 'Credential Access', + id: 'TA0006', + reference: 'https://attack.mitre.org/tactics/TA0006', + }, + { + name: 'Defense Evasion', + id: 'TA0005', + reference: 'https://attack.mitre.org/tactics/TA0005', + }, + { + name: 'Discovery', + id: 'TA0007', + reference: 'https://attack.mitre.org/tactics/TA0007', + }, + { + name: 'Execution', + id: 'TA0002', + reference: 'https://attack.mitre.org/tactics/TA0002', + }, + { + name: 'Exfiltration', + id: 'TA0010', + reference: 'https://attack.mitre.org/tactics/TA0010', + }, + { + name: 'Impact', + id: 'TA0040', + reference: 'https://attack.mitre.org/tactics/TA0040', + }, + { + name: 'Initial Access', + id: 'TA0001', + reference: 'https://attack.mitre.org/tactics/TA0001', + }, + { + name: 'Lateral Movement', + id: 'TA0008', + reference: 'https://attack.mitre.org/tactics/TA0008', + }, + { + name: 'Persistence', + id: 'TA0003', + reference: 'https://attack.mitre.org/tactics/TA0003', + }, + { + name: 'Privilege Escalation', + id: 'TA0004', + reference: 'https://attack.mitre.org/tactics/TA0004', + }, +]; + +export const tacticsOptions: MitreTacticsOptions[] = [ + { + id: 'TA0009', + name: 'Collection', + reference: 'https://attack.mitre.org/tactics/TA0009', + text: i18n.translate('xpack.siem.detectionEngine.mitreAttackTactics.collectionDescription', { + defaultMessage: 'Collection (TA0009)', + }), + value: 'collection', + }, + { + id: 'TA0011', + name: 'Command and Control', + reference: 'https://attack.mitre.org/tactics/TA0011', + text: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTactics.commandAndControlDescription', + { defaultMessage: 'Command and Control (TA0011)' } + ), + value: 'commandAndControl', + }, + { + id: 'TA0006', + name: 'Credential Access', + reference: 'https://attack.mitre.org/tactics/TA0006', + text: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTactics.credentialAccessDescription', + { defaultMessage: 'Credential Access (TA0006)' } + ), + value: 'credentialAccess', + }, + { + id: 'TA0005', + name: 'Defense Evasion', + reference: 'https://attack.mitre.org/tactics/TA0005', + text: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTactics.defenseEvasionDescription', + { defaultMessage: 'Defense Evasion (TA0005)' } + ), + value: 'defenseEvasion', + }, + { + id: 'TA0007', + name: 'Discovery', + reference: 'https://attack.mitre.org/tactics/TA0007', + text: i18n.translate('xpack.siem.detectionEngine.mitreAttackTactics.discoveryDescription', { + defaultMessage: 'Discovery (TA0007)', + }), + value: 'discovery', + }, + { + id: 'TA0002', + name: 'Execution', + reference: 'https://attack.mitre.org/tactics/TA0002', + text: i18n.translate('xpack.siem.detectionEngine.mitreAttackTactics.executionDescription', { + defaultMessage: 'Execution (TA0002)', + }), + value: 'execution', + }, + { + id: 'TA0010', + name: 'Exfiltration', + reference: 'https://attack.mitre.org/tactics/TA0010', + text: i18n.translate('xpack.siem.detectionEngine.mitreAttackTactics.exfiltrationDescription', { + defaultMessage: 'Exfiltration (TA0010)', + }), + value: 'exfiltration', + }, + { + id: 'TA0040', + name: 'Impact', + reference: 'https://attack.mitre.org/tactics/TA0040', + text: i18n.translate('xpack.siem.detectionEngine.mitreAttackTactics.impactDescription', { + defaultMessage: 'Impact (TA0040)', + }), + value: 'impact', + }, + { + id: 'TA0001', + name: 'Initial Access', + reference: 'https://attack.mitre.org/tactics/TA0001', + text: i18n.translate('xpack.siem.detectionEngine.mitreAttackTactics.initialAccessDescription', { + defaultMessage: 'Initial Access (TA0001)', + }), + value: 'initialAccess', + }, + { + id: 'TA0008', + name: 'Lateral Movement', + reference: 'https://attack.mitre.org/tactics/TA0008', + text: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTactics.lateralMovementDescription', + { defaultMessage: 'Lateral Movement (TA0008)' } + ), + value: 'lateralMovement', + }, + { + id: 'TA0003', + name: 'Persistence', + reference: 'https://attack.mitre.org/tactics/TA0003', + text: i18n.translate('xpack.siem.detectionEngine.mitreAttackTactics.persistenceDescription', { + defaultMessage: 'Persistence (TA0003)', + }), + value: 'persistence', + }, + { + id: 'TA0004', + name: 'Privilege Escalation', + reference: 'https://attack.mitre.org/tactics/TA0004', + text: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTactics.privilegeEscalationDescription', + { defaultMessage: 'Privilege Escalation (TA0004)' } + ), + value: 'privilegeEscalation', + }, +]; + +export const techniques = [ + { + name: '.bash_profile and .bashrc', + id: 'T1156', + reference: 'https://attack.mitre.org/techniques/T1156', + tactics: ['persistence'], + }, + { + name: 'Access Token Manipulation', + id: 'T1134', + reference: 'https://attack.mitre.org/techniques/T1134', + tactics: ['defense-evasion', 'privilege-escalation'], + }, + { + name: 'Accessibility Features', + id: 'T1015', + reference: 'https://attack.mitre.org/techniques/T1015', + tactics: ['persistence', 'privilege-escalation'], + }, + { + name: 'Account Access Removal', + id: 'T1531', + reference: 'https://attack.mitre.org/techniques/T1531', + tactics: ['impact'], + }, + { + name: 'Account Discovery', + id: 'T1087', + reference: 'https://attack.mitre.org/techniques/T1087', + tactics: ['discovery'], + }, + { + name: 'Account Manipulation', + id: 'T1098', + reference: 'https://attack.mitre.org/techniques/T1098', + tactics: ['credential-access', 'persistence'], + }, + { + name: 'AppCert DLLs', + id: 'T1182', + reference: 'https://attack.mitre.org/techniques/T1182', + tactics: ['persistence', 'privilege-escalation'], + }, + { + name: 'AppInit DLLs', + id: 'T1103', + reference: 'https://attack.mitre.org/techniques/T1103', + tactics: ['persistence', 'privilege-escalation'], + }, + { + name: 'AppleScript', + id: 'T1155', + reference: 'https://attack.mitre.org/techniques/T1155', + tactics: ['execution', 'lateral-movement'], + }, + { + name: 'Application Access Token', + id: 'T1527', + reference: 'https://attack.mitre.org/techniques/T1527', + tactics: ['defense-evasion', 'lateral-movement'], + }, + { + name: 'Application Deployment Software', + id: 'T1017', + reference: 'https://attack.mitre.org/techniques/T1017', + tactics: ['lateral-movement'], + }, + { + name: 'Application Shimming', + id: 'T1138', + reference: 'https://attack.mitre.org/techniques/T1138', + tactics: ['persistence', 'privilege-escalation'], + }, + { + name: 'Application Window Discovery', + id: 'T1010', + reference: 'https://attack.mitre.org/techniques/T1010', + tactics: ['discovery'], + }, + { + name: 'Audio Capture', + id: 'T1123', + reference: 'https://attack.mitre.org/techniques/T1123', + tactics: ['collection'], + }, + { + name: 'Authentication Package', + id: 'T1131', + reference: 'https://attack.mitre.org/techniques/T1131', + tactics: ['persistence'], + }, + { + name: 'Automated Collection', + id: 'T1119', + reference: 'https://attack.mitre.org/techniques/T1119', + tactics: ['collection'], + }, + { + name: 'Automated Exfiltration', + id: 'T1020', + reference: 'https://attack.mitre.org/techniques/T1020', + tactics: ['exfiltration'], + }, + { + name: 'BITS Jobs', + id: 'T1197', + reference: 'https://attack.mitre.org/techniques/T1197', + tactics: ['defense-evasion', 'persistence'], + }, + { + name: 'Bash History', + id: 'T1139', + reference: 'https://attack.mitre.org/techniques/T1139', + tactics: ['credential-access'], + }, + { + name: 'Binary Padding', + id: 'T1009', + reference: 'https://attack.mitre.org/techniques/T1009', + tactics: ['defense-evasion'], + }, + { + name: 'Bootkit', + id: 'T1067', + reference: 'https://attack.mitre.org/techniques/T1067', + tactics: ['persistence'], + }, + { + name: 'Browser Bookmark Discovery', + id: 'T1217', + reference: 'https://attack.mitre.org/techniques/T1217', + tactics: ['discovery'], + }, + { + name: 'Browser Extensions', + id: 'T1176', + reference: 'https://attack.mitre.org/techniques/T1176', + tactics: ['persistence'], + }, + { + name: 'Brute Force', + id: 'T1110', + reference: 'https://attack.mitre.org/techniques/T1110', + tactics: ['credential-access'], + }, + { + name: 'Bypass User Account Control', + id: 'T1088', + reference: 'https://attack.mitre.org/techniques/T1088', + tactics: ['defense-evasion', 'privilege-escalation'], + }, + { + name: 'CMSTP', + id: 'T1191', + reference: 'https://attack.mitre.org/techniques/T1191', + tactics: ['defense-evasion', 'execution'], + }, + { + name: 'Change Default File Association', + id: 'T1042', + reference: 'https://attack.mitre.org/techniques/T1042', + tactics: ['persistence'], + }, + { + name: 'Clear Command History', + id: 'T1146', + reference: 'https://attack.mitre.org/techniques/T1146', + tactics: ['defense-evasion'], + }, + { + name: 'Clipboard Data', + id: 'T1115', + reference: 'https://attack.mitre.org/techniques/T1115', + tactics: ['collection'], + }, + { + name: 'Cloud Instance Metadata API', + id: 'T1522', + reference: 'https://attack.mitre.org/techniques/T1522', + tactics: ['credential-access'], + }, + { + name: 'Cloud Service Dashboard', + id: 'T1538', + reference: 'https://attack.mitre.org/techniques/T1538', + tactics: ['discovery'], + }, + { + name: 'Cloud Service Discovery', + id: 'T1526', + reference: 'https://attack.mitre.org/techniques/T1526', + tactics: ['discovery'], + }, + { + name: 'Code Signing', + id: 'T1116', + reference: 'https://attack.mitre.org/techniques/T1116', + tactics: ['defense-evasion'], + }, + { + name: 'Command-Line Interface', + id: 'T1059', + reference: 'https://attack.mitre.org/techniques/T1059', + tactics: ['execution'], + }, + { + name: 'Commonly Used Port', + id: 'T1043', + reference: 'https://attack.mitre.org/techniques/T1043', + tactics: ['command-and-control'], + }, + { + name: 'Communication Through Removable Media', + id: 'T1092', + reference: 'https://attack.mitre.org/techniques/T1092', + tactics: ['command-and-control'], + }, + { + name: 'Compile After Delivery', + id: 'T1500', + reference: 'https://attack.mitre.org/techniques/T1500', + tactics: ['defense-evasion'], + }, + { + name: 'Compiled HTML File', + id: 'T1223', + reference: 'https://attack.mitre.org/techniques/T1223', + tactics: ['defense-evasion', 'execution'], + }, + { + name: 'Component Firmware', + id: 'T1109', + reference: 'https://attack.mitre.org/techniques/T1109', + tactics: ['defense-evasion', 'persistence'], + }, + { + name: 'Component Object Model Hijacking', + id: 'T1122', + reference: 'https://attack.mitre.org/techniques/T1122', + tactics: ['defense-evasion', 'persistence'], + }, + { + name: 'Component Object Model and Distributed COM', + id: 'T1175', + reference: 'https://attack.mitre.org/techniques/T1175', + tactics: ['lateral-movement', 'execution'], + }, + { + name: 'Connection Proxy', + id: 'T1090', + reference: 'https://attack.mitre.org/techniques/T1090', + tactics: ['command-and-control', 'defense-evasion'], + }, + { + name: 'Control Panel Items', + id: 'T1196', + reference: 'https://attack.mitre.org/techniques/T1196', + tactics: ['defense-evasion', 'execution'], + }, + { + name: 'Create Account', + id: 'T1136', + reference: 'https://attack.mitre.org/techniques/T1136', + tactics: ['persistence'], + }, + { + name: 'Credential Dumping', + id: 'T1003', + reference: 'https://attack.mitre.org/techniques/T1003', + tactics: ['credential-access'], + }, + { + name: 'Credentials from Web Browsers', + id: 'T1503', + reference: 'https://attack.mitre.org/techniques/T1503', + tactics: ['credential-access'], + }, + { + name: 'Credentials in Files', + id: 'T1081', + reference: 'https://attack.mitre.org/techniques/T1081', + tactics: ['credential-access'], + }, + { + name: 'Credentials in Registry', + id: 'T1214', + reference: 'https://attack.mitre.org/techniques/T1214', + tactics: ['credential-access'], + }, + { + name: 'Custom Command and Control Protocol', + id: 'T1094', + reference: 'https://attack.mitre.org/techniques/T1094', + tactics: ['command-and-control'], + }, + { + name: 'Custom Cryptographic Protocol', + id: 'T1024', + reference: 'https://attack.mitre.org/techniques/T1024', + tactics: ['command-and-control'], + }, + { + name: 'DCShadow', + id: 'T1207', + reference: 'https://attack.mitre.org/techniques/T1207', + tactics: ['defense-evasion'], + }, + { + name: 'DLL Search Order Hijacking', + id: 'T1038', + reference: 'https://attack.mitre.org/techniques/T1038', + tactics: ['persistence', 'privilege-escalation', 'defense-evasion'], + }, + { + name: 'DLL Side-Loading', + id: 'T1073', + reference: 'https://attack.mitre.org/techniques/T1073', + tactics: ['defense-evasion'], + }, + { + name: 'Data Compressed', + id: 'T1002', + reference: 'https://attack.mitre.org/techniques/T1002', + tactics: ['exfiltration'], + }, + { + name: 'Data Destruction', + id: 'T1485', + reference: 'https://attack.mitre.org/techniques/T1485', + tactics: ['impact'], + }, + { + name: 'Data Encoding', + id: 'T1132', + reference: 'https://attack.mitre.org/techniques/T1132', + tactics: ['command-and-control'], + }, + { + name: 'Data Encrypted', + id: 'T1022', + reference: 'https://attack.mitre.org/techniques/T1022', + tactics: ['exfiltration'], + }, + { + name: 'Data Encrypted for Impact', + id: 'T1486', + reference: 'https://attack.mitre.org/techniques/T1486', + tactics: ['impact'], + }, + { + name: 'Data Obfuscation', + id: 'T1001', + reference: 'https://attack.mitre.org/techniques/T1001', + tactics: ['command-and-control'], + }, + { + name: 'Data Staged', + id: 'T1074', + reference: 'https://attack.mitre.org/techniques/T1074', + tactics: ['collection'], + }, + { + name: 'Data Transfer Size Limits', + id: 'T1030', + reference: 'https://attack.mitre.org/techniques/T1030', + tactics: ['exfiltration'], + }, + { + name: 'Data from Cloud Storage Object', + id: 'T1530', + reference: 'https://attack.mitre.org/techniques/T1530', + tactics: ['collection'], + }, + { + name: 'Data from Information Repositories', + id: 'T1213', + reference: 'https://attack.mitre.org/techniques/T1213', + tactics: ['collection'], + }, + { + name: 'Data from Local System', + id: 'T1005', + reference: 'https://attack.mitre.org/techniques/T1005', + tactics: ['collection'], + }, + { + name: 'Data from Network Shared Drive', + id: 'T1039', + reference: 'https://attack.mitre.org/techniques/T1039', + tactics: ['collection'], + }, + { + name: 'Data from Removable Media', + id: 'T1025', + reference: 'https://attack.mitre.org/techniques/T1025', + tactics: ['collection'], + }, + { + name: 'Defacement', + id: 'T1491', + reference: 'https://attack.mitre.org/techniques/T1491', + tactics: ['impact'], + }, + { + name: 'Deobfuscate/Decode Files or Information', + id: 'T1140', + reference: 'https://attack.mitre.org/techniques/T1140', + tactics: ['defense-evasion'], + }, + { + name: 'Disabling Security Tools', + id: 'T1089', + reference: 'https://attack.mitre.org/techniques/T1089', + tactics: ['defense-evasion'], + }, + { + name: 'Disk Content Wipe', + id: 'T1488', + reference: 'https://attack.mitre.org/techniques/T1488', + tactics: ['impact'], + }, + { + name: 'Disk Structure Wipe', + id: 'T1487', + reference: 'https://attack.mitre.org/techniques/T1487', + tactics: ['impact'], + }, + { + name: 'Domain Fronting', + id: 'T1172', + reference: 'https://attack.mitre.org/techniques/T1172', + tactics: ['command-and-control'], + }, + { + name: 'Domain Generation Algorithms', + id: 'T1483', + reference: 'https://attack.mitre.org/techniques/T1483', + tactics: ['command-and-control'], + }, + { + name: 'Domain Trust Discovery', + id: 'T1482', + reference: 'https://attack.mitre.org/techniques/T1482', + tactics: ['discovery'], + }, + { + name: 'Drive-by Compromise', + id: 'T1189', + reference: 'https://attack.mitre.org/techniques/T1189', + tactics: ['initial-access'], + }, + { + name: 'Dylib Hijacking', + id: 'T1157', + reference: 'https://attack.mitre.org/techniques/T1157', + tactics: ['persistence', 'privilege-escalation'], + }, + { + name: 'Dynamic Data Exchange', + id: 'T1173', + reference: 'https://attack.mitre.org/techniques/T1173', + tactics: ['execution'], + }, + { + name: 'Elevated Execution with Prompt', + id: 'T1514', + reference: 'https://attack.mitre.org/techniques/T1514', + tactics: ['privilege-escalation'], + }, + { + name: 'Email Collection', + id: 'T1114', + reference: 'https://attack.mitre.org/techniques/T1114', + tactics: ['collection'], + }, + { + name: 'Emond', + id: 'T1519', + reference: 'https://attack.mitre.org/techniques/T1519', + tactics: ['persistence', 'privilege-escalation'], + }, + { + name: 'Endpoint Denial of Service', + id: 'T1499', + reference: 'https://attack.mitre.org/techniques/T1499', + tactics: ['impact'], + }, + { + name: 'Execution Guardrails', + id: 'T1480', + reference: 'https://attack.mitre.org/techniques/T1480', + tactics: ['defense-evasion'], + }, + { + name: 'Execution through API', + id: 'T1106', + reference: 'https://attack.mitre.org/techniques/T1106', + tactics: ['execution'], + }, + { + name: 'Execution through Module Load', + id: 'T1129', + reference: 'https://attack.mitre.org/techniques/T1129', + tactics: ['execution'], + }, + { + name: 'Exfiltration Over Alternative Protocol', + id: 'T1048', + reference: 'https://attack.mitre.org/techniques/T1048', + tactics: ['exfiltration'], + }, + { + name: 'Exfiltration Over Command and Control Channel', + id: 'T1041', + reference: 'https://attack.mitre.org/techniques/T1041', + tactics: ['exfiltration'], + }, + { + name: 'Exfiltration Over Other Network Medium', + id: 'T1011', + reference: 'https://attack.mitre.org/techniques/T1011', + tactics: ['exfiltration'], + }, + { + name: 'Exfiltration Over Physical Medium', + id: 'T1052', + reference: 'https://attack.mitre.org/techniques/T1052', + tactics: ['exfiltration'], + }, + { + name: 'Exploit Public-Facing Application', + id: 'T1190', + reference: 'https://attack.mitre.org/techniques/T1190', + tactics: ['initial-access'], + }, + { + name: 'Exploitation for Client Execution', + id: 'T1203', + reference: 'https://attack.mitre.org/techniques/T1203', + tactics: ['execution'], + }, + { + name: 'Exploitation for Credential Access', + id: 'T1212', + reference: 'https://attack.mitre.org/techniques/T1212', + tactics: ['credential-access'], + }, + { + name: 'Exploitation for Defense Evasion', + id: 'T1211', + reference: 'https://attack.mitre.org/techniques/T1211', + tactics: ['defense-evasion'], + }, + { + name: 'Exploitation for Privilege Escalation', + id: 'T1068', + reference: 'https://attack.mitre.org/techniques/T1068', + tactics: ['privilege-escalation'], + }, + { + name: 'Exploitation of Remote Services', + id: 'T1210', + reference: 'https://attack.mitre.org/techniques/T1210', + tactics: ['lateral-movement'], + }, + { + name: 'External Remote Services', + id: 'T1133', + reference: 'https://attack.mitre.org/techniques/T1133', + tactics: ['persistence', 'initial-access'], + }, + { + name: 'Extra Window Memory Injection', + id: 'T1181', + reference: 'https://attack.mitre.org/techniques/T1181', + tactics: ['defense-evasion', 'privilege-escalation'], + }, + { + name: 'Fallback Channels', + id: 'T1008', + reference: 'https://attack.mitre.org/techniques/T1008', + tactics: ['command-and-control'], + }, + { + name: 'File Deletion', + id: 'T1107', + reference: 'https://attack.mitre.org/techniques/T1107', + tactics: ['defense-evasion'], + }, + { + name: 'File System Logical Offsets', + id: 'T1006', + reference: 'https://attack.mitre.org/techniques/T1006', + tactics: ['defense-evasion'], + }, + { + name: 'File System Permissions Weakness', + id: 'T1044', + reference: 'https://attack.mitre.org/techniques/T1044', + tactics: ['persistence', 'privilege-escalation'], + }, + { + name: 'File and Directory Discovery', + id: 'T1083', + reference: 'https://attack.mitre.org/techniques/T1083', + tactics: ['discovery'], + }, + { + name: 'File and Directory Permissions Modification', + id: 'T1222', + reference: 'https://attack.mitre.org/techniques/T1222', + tactics: ['defense-evasion'], + }, + { + name: 'Firmware Corruption', + id: 'T1495', + reference: 'https://attack.mitre.org/techniques/T1495', + tactics: ['impact'], + }, + { + name: 'Forced Authentication', + id: 'T1187', + reference: 'https://attack.mitre.org/techniques/T1187', + tactics: ['credential-access'], + }, + { + name: 'Gatekeeper Bypass', + id: 'T1144', + reference: 'https://attack.mitre.org/techniques/T1144', + tactics: ['defense-evasion'], + }, + { + name: 'Graphical User Interface', + id: 'T1061', + reference: 'https://attack.mitre.org/techniques/T1061', + tactics: ['execution'], + }, + { + name: 'Group Policy Modification', + id: 'T1484', + reference: 'https://attack.mitre.org/techniques/T1484', + tactics: ['defense-evasion'], + }, + { + name: 'HISTCONTROL', + id: 'T1148', + reference: 'https://attack.mitre.org/techniques/T1148', + tactics: ['defense-evasion'], + }, + { + name: 'Hardware Additions', + id: 'T1200', + reference: 'https://attack.mitre.org/techniques/T1200', + tactics: ['initial-access'], + }, + { + name: 'Hidden Files and Directories', + id: 'T1158', + reference: 'https://attack.mitre.org/techniques/T1158', + tactics: ['defense-evasion', 'persistence'], + }, + { + name: 'Hidden Users', + id: 'T1147', + reference: 'https://attack.mitre.org/techniques/T1147', + tactics: ['defense-evasion'], + }, + { + name: 'Hidden Window', + id: 'T1143', + reference: 'https://attack.mitre.org/techniques/T1143', + tactics: ['defense-evasion'], + }, + { + name: 'Hooking', + id: 'T1179', + reference: 'https://attack.mitre.org/techniques/T1179', + tactics: ['persistence', 'privilege-escalation', 'credential-access'], + }, + { + name: 'Hypervisor', + id: 'T1062', + reference: 'https://attack.mitre.org/techniques/T1062', + tactics: ['persistence'], + }, + { + name: 'Image File Execution Options Injection', + id: 'T1183', + reference: 'https://attack.mitre.org/techniques/T1183', + tactics: ['privilege-escalation', 'persistence', 'defense-evasion'], + }, + { + name: 'Implant Container Image', + id: 'T1525', + reference: 'https://attack.mitre.org/techniques/T1525', + tactics: ['persistence'], + }, + { + name: 'Indicator Blocking', + id: 'T1054', + reference: 'https://attack.mitre.org/techniques/T1054', + tactics: ['defense-evasion'], + }, + { + name: 'Indicator Removal from Tools', + id: 'T1066', + reference: 'https://attack.mitre.org/techniques/T1066', + tactics: ['defense-evasion'], + }, + { + name: 'Indicator Removal on Host', + id: 'T1070', + reference: 'https://attack.mitre.org/techniques/T1070', + tactics: ['defense-evasion'], + }, + { + name: 'Indirect Command Execution', + id: 'T1202', + reference: 'https://attack.mitre.org/techniques/T1202', + tactics: ['defense-evasion'], + }, + { + name: 'Inhibit System Recovery', + id: 'T1490', + reference: 'https://attack.mitre.org/techniques/T1490', + tactics: ['impact'], + }, + { + name: 'Input Capture', + id: 'T1056', + reference: 'https://attack.mitre.org/techniques/T1056', + tactics: ['collection', 'credential-access'], + }, + { + name: 'Input Prompt', + id: 'T1141', + reference: 'https://attack.mitre.org/techniques/T1141', + tactics: ['credential-access'], + }, + { + name: 'Install Root Certificate', + id: 'T1130', + reference: 'https://attack.mitre.org/techniques/T1130', + tactics: ['defense-evasion'], + }, + { + name: 'InstallUtil', + id: 'T1118', + reference: 'https://attack.mitre.org/techniques/T1118', + tactics: ['defense-evasion', 'execution'], + }, + { + name: 'Internal Spearphishing', + id: 'T1534', + reference: 'https://attack.mitre.org/techniques/T1534', + tactics: ['lateral-movement'], + }, + { + name: 'Kerberoasting', + id: 'T1208', + reference: 'https://attack.mitre.org/techniques/T1208', + tactics: ['credential-access'], + }, + { + name: 'Kernel Modules and Extensions', + id: 'T1215', + reference: 'https://attack.mitre.org/techniques/T1215', + tactics: ['persistence'], + }, + { + name: 'Keychain', + id: 'T1142', + reference: 'https://attack.mitre.org/techniques/T1142', + tactics: ['credential-access'], + }, + { + name: 'LC_LOAD_DYLIB Addition', + id: 'T1161', + reference: 'https://attack.mitre.org/techniques/T1161', + tactics: ['persistence'], + }, + { + name: 'LC_MAIN Hijacking', + id: 'T1149', + reference: 'https://attack.mitre.org/techniques/T1149', + tactics: ['defense-evasion'], + }, + { + name: 'LLMNR/NBT-NS Poisoning and Relay', + id: 'T1171', + reference: 'https://attack.mitre.org/techniques/T1171', + tactics: ['credential-access'], + }, + { + name: 'LSASS Driver', + id: 'T1177', + reference: 'https://attack.mitre.org/techniques/T1177', + tactics: ['execution', 'persistence'], + }, + { + name: 'Launch Agent', + id: 'T1159', + reference: 'https://attack.mitre.org/techniques/T1159', + tactics: ['persistence'], + }, + { + name: 'Launch Daemon', + id: 'T1160', + reference: 'https://attack.mitre.org/techniques/T1160', + tactics: ['persistence', 'privilege-escalation'], + }, + { + name: 'Launchctl', + id: 'T1152', + reference: 'https://attack.mitre.org/techniques/T1152', + tactics: ['defense-evasion', 'execution', 'persistence'], + }, + { + name: 'Local Job Scheduling', + id: 'T1168', + reference: 'https://attack.mitre.org/techniques/T1168', + tactics: ['persistence', 'execution'], + }, + { + name: 'Login Item', + id: 'T1162', + reference: 'https://attack.mitre.org/techniques/T1162', + tactics: ['persistence'], + }, + { + name: 'Logon Scripts', + id: 'T1037', + reference: 'https://attack.mitre.org/techniques/T1037', + tactics: ['lateral-movement', 'persistence'], + }, + { + name: 'Man in the Browser', + id: 'T1185', + reference: 'https://attack.mitre.org/techniques/T1185', + tactics: ['collection'], + }, + { + name: 'Masquerading', + id: 'T1036', + reference: 'https://attack.mitre.org/techniques/T1036', + tactics: ['defense-evasion'], + }, + { + name: 'Modify Existing Service', + id: 'T1031', + reference: 'https://attack.mitre.org/techniques/T1031', + tactics: ['persistence'], + }, + { + name: 'Modify Registry', + id: 'T1112', + reference: 'https://attack.mitre.org/techniques/T1112', + tactics: ['defense-evasion'], + }, + { + name: 'Mshta', + id: 'T1170', + reference: 'https://attack.mitre.org/techniques/T1170', + tactics: ['defense-evasion', 'execution'], + }, + { + name: 'Multi-Stage Channels', + id: 'T1104', + reference: 'https://attack.mitre.org/techniques/T1104', + tactics: ['command-and-control'], + }, + { + name: 'Multi-hop Proxy', + id: 'T1188', + reference: 'https://attack.mitre.org/techniques/T1188', + tactics: ['command-and-control'], + }, + { + name: 'Multiband Communication', + id: 'T1026', + reference: 'https://attack.mitre.org/techniques/T1026', + tactics: ['command-and-control'], + }, + { + name: 'Multilayer Encryption', + id: 'T1079', + reference: 'https://attack.mitre.org/techniques/T1079', + tactics: ['command-and-control'], + }, + { + name: 'NTFS File Attributes', + id: 'T1096', + reference: 'https://attack.mitre.org/techniques/T1096', + tactics: ['defense-evasion'], + }, + { + name: 'Netsh Helper DLL', + id: 'T1128', + reference: 'https://attack.mitre.org/techniques/T1128', + tactics: ['persistence'], + }, + { + name: 'Network Denial of Service', + id: 'T1498', + reference: 'https://attack.mitre.org/techniques/T1498', + tactics: ['impact'], + }, + { + name: 'Network Service Scanning', + id: 'T1046', + reference: 'https://attack.mitre.org/techniques/T1046', + tactics: ['discovery'], + }, + { + name: 'Network Share Connection Removal', + id: 'T1126', + reference: 'https://attack.mitre.org/techniques/T1126', + tactics: ['defense-evasion'], + }, + { + name: 'Network Share Discovery', + id: 'T1135', + reference: 'https://attack.mitre.org/techniques/T1135', + tactics: ['discovery'], + }, + { + name: 'Network Sniffing', + id: 'T1040', + reference: 'https://attack.mitre.org/techniques/T1040', + tactics: ['credential-access', 'discovery'], + }, + { + name: 'New Service', + id: 'T1050', + reference: 'https://attack.mitre.org/techniques/T1050', + tactics: ['persistence', 'privilege-escalation'], + }, + { + name: 'Obfuscated Files or Information', + id: 'T1027', + reference: 'https://attack.mitre.org/techniques/T1027', + tactics: ['defense-evasion'], + }, + { + name: 'Office Application Startup', + id: 'T1137', + reference: 'https://attack.mitre.org/techniques/T1137', + tactics: ['persistence'], + }, + { + name: 'Parent PID Spoofing', + id: 'T1502', + reference: 'https://attack.mitre.org/techniques/T1502', + tactics: ['defense-evasion', 'privilege-escalation'], + }, + { + name: 'Pass the Hash', + id: 'T1075', + reference: 'https://attack.mitre.org/techniques/T1075', + tactics: ['lateral-movement'], + }, + { + name: 'Pass the Ticket', + id: 'T1097', + reference: 'https://attack.mitre.org/techniques/T1097', + tactics: ['lateral-movement'], + }, + { + name: 'Password Filter DLL', + id: 'T1174', + reference: 'https://attack.mitre.org/techniques/T1174', + tactics: ['credential-access'], + }, + { + name: 'Password Policy Discovery', + id: 'T1201', + reference: 'https://attack.mitre.org/techniques/T1201', + tactics: ['discovery'], + }, + { + name: 'Path Interception', + id: 'T1034', + reference: 'https://attack.mitre.org/techniques/T1034', + tactics: ['persistence', 'privilege-escalation'], + }, + { + name: 'Peripheral Device Discovery', + id: 'T1120', + reference: 'https://attack.mitre.org/techniques/T1120', + tactics: ['discovery'], + }, + { + name: 'Permission Groups Discovery', + id: 'T1069', + reference: 'https://attack.mitre.org/techniques/T1069', + tactics: ['discovery'], + }, + { + name: 'Plist Modification', + id: 'T1150', + reference: 'https://attack.mitre.org/techniques/T1150', + tactics: ['defense-evasion', 'persistence', 'privilege-escalation'], + }, + { + name: 'Port Knocking', + id: 'T1205', + reference: 'https://attack.mitre.org/techniques/T1205', + tactics: ['defense-evasion', 'persistence', 'command-and-control'], + }, + { + name: 'Port Monitors', + id: 'T1013', + reference: 'https://attack.mitre.org/techniques/T1013', + tactics: ['persistence', 'privilege-escalation'], + }, + { + name: 'PowerShell', + id: 'T1086', + reference: 'https://attack.mitre.org/techniques/T1086', + tactics: ['execution'], + }, + { + name: 'PowerShell Profile', + id: 'T1504', + reference: 'https://attack.mitre.org/techniques/T1504', + tactics: ['persistence', 'privilege-escalation'], + }, + { + name: 'Private Keys', + id: 'T1145', + reference: 'https://attack.mitre.org/techniques/T1145', + tactics: ['credential-access'], + }, + { + name: 'Process Discovery', + id: 'T1057', + reference: 'https://attack.mitre.org/techniques/T1057', + tactics: ['discovery'], + }, + { + name: 'Process Doppelgänging', + id: 'T1186', + reference: 'https://attack.mitre.org/techniques/T1186', + tactics: ['defense-evasion'], + }, + { + name: 'Process Hollowing', + id: 'T1093', + reference: 'https://attack.mitre.org/techniques/T1093', + tactics: ['defense-evasion'], + }, + { + name: 'Process Injection', + id: 'T1055', + reference: 'https://attack.mitre.org/techniques/T1055', + tactics: ['defense-evasion', 'privilege-escalation'], + }, + { + name: 'Query Registry', + id: 'T1012', + reference: 'https://attack.mitre.org/techniques/T1012', + tactics: ['discovery'], + }, + { + name: 'Rc.common', + id: 'T1163', + reference: 'https://attack.mitre.org/techniques/T1163', + tactics: ['persistence'], + }, + { + name: 'Re-opened Applications', + id: 'T1164', + reference: 'https://attack.mitre.org/techniques/T1164', + tactics: ['persistence'], + }, + { + name: 'Redundant Access', + id: 'T1108', + reference: 'https://attack.mitre.org/techniques/T1108', + tactics: ['defense-evasion', 'persistence'], + }, + { + name: 'Registry Run Keys / Startup Folder', + id: 'T1060', + reference: 'https://attack.mitre.org/techniques/T1060', + tactics: ['persistence'], + }, + { + name: 'Regsvcs/Regasm', + id: 'T1121', + reference: 'https://attack.mitre.org/techniques/T1121', + tactics: ['defense-evasion', 'execution'], + }, + { + name: 'Regsvr32', + id: 'T1117', + reference: 'https://attack.mitre.org/techniques/T1117', + tactics: ['defense-evasion', 'execution'], + }, + { + name: 'Remote Access Tools', + id: 'T1219', + reference: 'https://attack.mitre.org/techniques/T1219', + tactics: ['command-and-control'], + }, + { + name: 'Remote Desktop Protocol', + id: 'T1076', + reference: 'https://attack.mitre.org/techniques/T1076', + tactics: ['lateral-movement'], + }, + { + name: 'Remote File Copy', + id: 'T1105', + reference: 'https://attack.mitre.org/techniques/T1105', + tactics: ['command-and-control', 'lateral-movement'], + }, + { + name: 'Remote Services', + id: 'T1021', + reference: 'https://attack.mitre.org/techniques/T1021', + tactics: ['lateral-movement'], + }, + { + name: 'Remote System Discovery', + id: 'T1018', + reference: 'https://attack.mitre.org/techniques/T1018', + tactics: ['discovery'], + }, + { + name: 'Replication Through Removable Media', + id: 'T1091', + reference: 'https://attack.mitre.org/techniques/T1091', + tactics: ['lateral-movement', 'initial-access'], + }, + { + name: 'Resource Hijacking', + id: 'T1496', + reference: 'https://attack.mitre.org/techniques/T1496', + tactics: ['impact'], + }, + { + name: 'Revert Cloud Instance', + id: 'T1536', + reference: 'https://attack.mitre.org/techniques/T1536', + tactics: ['defense-evasion'], + }, + { + name: 'Rootkit', + id: 'T1014', + reference: 'https://attack.mitre.org/techniques/T1014', + tactics: ['defense-evasion'], + }, + { + name: 'Rundll32', + id: 'T1085', + reference: 'https://attack.mitre.org/techniques/T1085', + tactics: ['defense-evasion', 'execution'], + }, + { + name: 'Runtime Data Manipulation', + id: 'T1494', + reference: 'https://attack.mitre.org/techniques/T1494', + tactics: ['impact'], + }, + { + name: 'SID-History Injection', + id: 'T1178', + reference: 'https://attack.mitre.org/techniques/T1178', + tactics: ['privilege-escalation'], + }, + { + name: 'SIP and Trust Provider Hijacking', + id: 'T1198', + reference: 'https://attack.mitre.org/techniques/T1198', + tactics: ['defense-evasion', 'persistence'], + }, + { + name: 'SSH Hijacking', + id: 'T1184', + reference: 'https://attack.mitre.org/techniques/T1184', + tactics: ['lateral-movement'], + }, + { + name: 'Scheduled Task', + id: 'T1053', + reference: 'https://attack.mitre.org/techniques/T1053', + tactics: ['execution', 'persistence', 'privilege-escalation'], + }, + { + name: 'Scheduled Transfer', + id: 'T1029', + reference: 'https://attack.mitre.org/techniques/T1029', + tactics: ['exfiltration'], + }, + { + name: 'Screen Capture', + id: 'T1113', + reference: 'https://attack.mitre.org/techniques/T1113', + tactics: ['collection'], + }, + { + name: 'Screensaver', + id: 'T1180', + reference: 'https://attack.mitre.org/techniques/T1180', + tactics: ['persistence'], + }, + { + name: 'Scripting', + id: 'T1064', + reference: 'https://attack.mitre.org/techniques/T1064', + tactics: ['defense-evasion', 'execution'], + }, + { + name: 'Security Software Discovery', + id: 'T1063', + reference: 'https://attack.mitre.org/techniques/T1063', + tactics: ['discovery'], + }, + { + name: 'Security Support Provider', + id: 'T1101', + reference: 'https://attack.mitre.org/techniques/T1101', + tactics: ['persistence'], + }, + { + name: 'Securityd Memory', + id: 'T1167', + reference: 'https://attack.mitre.org/techniques/T1167', + tactics: ['credential-access'], + }, + { + name: 'Server Software Component', + id: 'T1505', + reference: 'https://attack.mitre.org/techniques/T1505', + tactics: ['persistence'], + }, + { + name: 'Service Execution', + id: 'T1035', + reference: 'https://attack.mitre.org/techniques/T1035', + tactics: ['execution'], + }, + { + name: 'Service Registry Permissions Weakness', + id: 'T1058', + reference: 'https://attack.mitre.org/techniques/T1058', + tactics: ['persistence', 'privilege-escalation'], + }, + { + name: 'Service Stop', + id: 'T1489', + reference: 'https://attack.mitre.org/techniques/T1489', + tactics: ['impact'], + }, + { + name: 'Setuid and Setgid', + id: 'T1166', + reference: 'https://attack.mitre.org/techniques/T1166', + tactics: ['privilege-escalation', 'persistence'], + }, + { + name: 'Shared Webroot', + id: 'T1051', + reference: 'https://attack.mitre.org/techniques/T1051', + tactics: ['lateral-movement'], + }, + { + name: 'Shortcut Modification', + id: 'T1023', + reference: 'https://attack.mitre.org/techniques/T1023', + tactics: ['persistence'], + }, + { + name: 'Signed Binary Proxy Execution', + id: 'T1218', + reference: 'https://attack.mitre.org/techniques/T1218', + tactics: ['defense-evasion', 'execution'], + }, + { + name: 'Signed Script Proxy Execution', + id: 'T1216', + reference: 'https://attack.mitre.org/techniques/T1216', + tactics: ['defense-evasion', 'execution'], + }, + { + name: 'Software Discovery', + id: 'T1518', + reference: 'https://attack.mitre.org/techniques/T1518', + tactics: ['discovery'], + }, + { + name: 'Software Packing', + id: 'T1045', + reference: 'https://attack.mitre.org/techniques/T1045', + tactics: ['defense-evasion'], + }, + { + name: 'Source', + id: 'T1153', + reference: 'https://attack.mitre.org/techniques/T1153', + tactics: ['execution'], + }, + { + name: 'Space after Filename', + id: 'T1151', + reference: 'https://attack.mitre.org/techniques/T1151', + tactics: ['defense-evasion', 'execution'], + }, + { + name: 'Spearphishing Attachment', + id: 'T1193', + reference: 'https://attack.mitre.org/techniques/T1193', + tactics: ['initial-access'], + }, + { + name: 'Spearphishing Link', + id: 'T1192', + reference: 'https://attack.mitre.org/techniques/T1192', + tactics: ['initial-access'], + }, + { + name: 'Spearphishing via Service', + id: 'T1194', + reference: 'https://attack.mitre.org/techniques/T1194', + tactics: ['initial-access'], + }, + { + name: 'Standard Application Layer Protocol', + id: 'T1071', + reference: 'https://attack.mitre.org/techniques/T1071', + tactics: ['command-and-control'], + }, + { + name: 'Standard Cryptographic Protocol', + id: 'T1032', + reference: 'https://attack.mitre.org/techniques/T1032', + tactics: ['command-and-control'], + }, + { + name: 'Standard Non-Application Layer Protocol', + id: 'T1095', + reference: 'https://attack.mitre.org/techniques/T1095', + tactics: ['command-and-control'], + }, + { + name: 'Startup Items', + id: 'T1165', + reference: 'https://attack.mitre.org/techniques/T1165', + tactics: ['persistence', 'privilege-escalation'], + }, + { + name: 'Steal Application Access Token', + id: 'T1528', + reference: 'https://attack.mitre.org/techniques/T1528', + tactics: ['credential-access'], + }, + { + name: 'Steal Web Session Cookie', + id: 'T1539', + reference: 'https://attack.mitre.org/techniques/T1539', + tactics: ['credential-access'], + }, + { + name: 'Stored Data Manipulation', + id: 'T1492', + reference: 'https://attack.mitre.org/techniques/T1492', + tactics: ['impact'], + }, + { + name: 'Sudo', + id: 'T1169', + reference: 'https://attack.mitre.org/techniques/T1169', + tactics: ['privilege-escalation'], + }, + { + name: 'Sudo Caching', + id: 'T1206', + reference: 'https://attack.mitre.org/techniques/T1206', + tactics: ['privilege-escalation'], + }, + { + name: 'Supply Chain Compromise', + id: 'T1195', + reference: 'https://attack.mitre.org/techniques/T1195', + tactics: ['initial-access'], + }, + { + name: 'System Firmware', + id: 'T1019', + reference: 'https://attack.mitre.org/techniques/T1019', + tactics: ['persistence'], + }, + { + name: 'System Information Discovery', + id: 'T1082', + reference: 'https://attack.mitre.org/techniques/T1082', + tactics: ['discovery'], + }, + { + name: 'System Network Configuration Discovery', + id: 'T1016', + reference: 'https://attack.mitre.org/techniques/T1016', + tactics: ['discovery'], + }, + { + name: 'System Network Connections Discovery', + id: 'T1049', + reference: 'https://attack.mitre.org/techniques/T1049', + tactics: ['discovery'], + }, + { + name: 'System Owner/User Discovery', + id: 'T1033', + reference: 'https://attack.mitre.org/techniques/T1033', + tactics: ['discovery'], + }, + { + name: 'System Service Discovery', + id: 'T1007', + reference: 'https://attack.mitre.org/techniques/T1007', + tactics: ['discovery'], + }, + { + name: 'System Shutdown/Reboot', + id: 'T1529', + reference: 'https://attack.mitre.org/techniques/T1529', + tactics: ['impact'], + }, + { + name: 'System Time Discovery', + id: 'T1124', + reference: 'https://attack.mitre.org/techniques/T1124', + tactics: ['discovery'], + }, + { + name: 'Systemd Service', + id: 'T1501', + reference: 'https://attack.mitre.org/techniques/T1501', + tactics: ['persistence'], + }, + { + name: 'Taint Shared Content', + id: 'T1080', + reference: 'https://attack.mitre.org/techniques/T1080', + tactics: ['lateral-movement'], + }, + { + name: 'Template Injection', + id: 'T1221', + reference: 'https://attack.mitre.org/techniques/T1221', + tactics: ['defense-evasion'], + }, + { + name: 'Third-party Software', + id: 'T1072', + reference: 'https://attack.mitre.org/techniques/T1072', + tactics: ['execution', 'lateral-movement'], + }, + { + name: 'Time Providers', + id: 'T1209', + reference: 'https://attack.mitre.org/techniques/T1209', + tactics: ['persistence'], + }, + { + name: 'Timestomp', + id: 'T1099', + reference: 'https://attack.mitre.org/techniques/T1099', + tactics: ['defense-evasion'], + }, + { + name: 'Transfer Data to Cloud Account', + id: 'T1537', + reference: 'https://attack.mitre.org/techniques/T1537', + tactics: ['exfiltration'], + }, + { + name: 'Transmitted Data Manipulation', + id: 'T1493', + reference: 'https://attack.mitre.org/techniques/T1493', + tactics: ['impact'], + }, + { + name: 'Trap', + id: 'T1154', + reference: 'https://attack.mitre.org/techniques/T1154', + tactics: ['execution', 'persistence'], + }, + { + name: 'Trusted Developer Utilities', + id: 'T1127', + reference: 'https://attack.mitre.org/techniques/T1127', + tactics: ['defense-evasion', 'execution'], + }, + { + name: 'Trusted Relationship', + id: 'T1199', + reference: 'https://attack.mitre.org/techniques/T1199', + tactics: ['initial-access'], + }, + { + name: 'Two-Factor Authentication Interception', + id: 'T1111', + reference: 'https://attack.mitre.org/techniques/T1111', + tactics: ['credential-access'], + }, + { + name: 'Uncommonly Used Port', + id: 'T1065', + reference: 'https://attack.mitre.org/techniques/T1065', + tactics: ['command-and-control'], + }, + { + name: 'Unused/Unsupported Cloud Regions', + id: 'T1535', + reference: 'https://attack.mitre.org/techniques/T1535', + tactics: ['defense-evasion'], + }, + { + name: 'User Execution', + id: 'T1204', + reference: 'https://attack.mitre.org/techniques/T1204', + tactics: ['execution'], + }, + { + name: 'Valid Accounts', + id: 'T1078', + reference: 'https://attack.mitre.org/techniques/T1078', + tactics: ['defense-evasion', 'persistence', 'privilege-escalation', 'initial-access'], + }, + { + name: 'Video Capture', + id: 'T1125', + reference: 'https://attack.mitre.org/techniques/T1125', + tactics: ['collection'], + }, + { + name: 'Virtualization/Sandbox Evasion', + id: 'T1497', + reference: 'https://attack.mitre.org/techniques/T1497', + tactics: ['defense-evasion', 'discovery'], + }, + { + name: 'Web Service', + id: 'T1102', + reference: 'https://attack.mitre.org/techniques/T1102', + tactics: ['command-and-control', 'defense-evasion'], + }, + { + name: 'Web Session Cookie', + id: 'T1506', + reference: 'https://attack.mitre.org/techniques/T1506', + tactics: ['defense-evasion', 'lateral-movement'], + }, + { + name: 'Web Shell', + id: 'T1100', + reference: 'https://attack.mitre.org/techniques/T1100', + tactics: ['persistence', 'privilege-escalation'], + }, + { + name: 'Windows Admin Shares', + id: 'T1077', + reference: 'https://attack.mitre.org/techniques/T1077', + tactics: ['lateral-movement'], + }, + { + name: 'Windows Management Instrumentation', + id: 'T1047', + reference: 'https://attack.mitre.org/techniques/T1047', + tactics: ['execution'], + }, + { + name: 'Windows Management Instrumentation Event Subscription', + id: 'T1084', + reference: 'https://attack.mitre.org/techniques/T1084', + tactics: ['persistence'], + }, + { + name: 'Windows Remote Management', + id: 'T1028', + reference: 'https://attack.mitre.org/techniques/T1028', + tactics: ['execution', 'lateral-movement'], + }, + { + name: 'Winlogon Helper DLL', + id: 'T1004', + reference: 'https://attack.mitre.org/techniques/T1004', + tactics: ['persistence'], + }, + { + name: 'XSL Script Processing', + id: 'T1220', + reference: 'https://attack.mitre.org/techniques/T1220', + tactics: ['defense-evasion', 'execution'], + }, +]; + +export const techniquesOptions: MitreTechniquesOptions[] = [ + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.bashProfileAndBashrcDescription', + { defaultMessage: '.bash_profile and .bashrc (T1156)' } + ), + id: 'T1156', + name: '.bash_profile and .bashrc', + reference: 'https://attack.mitre.org/techniques/T1156', + tactics: 'persistence', + value: 'bashProfileAndBashrc', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.accessTokenManipulationDescription', + { defaultMessage: 'Access Token Manipulation (T1134)' } + ), + id: 'T1134', + name: 'Access Token Manipulation', + reference: 'https://attack.mitre.org/techniques/T1134', + tactics: 'defense-evasion,privilege-escalation', + value: 'accessTokenManipulation', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.accessibilityFeaturesDescription', + { defaultMessage: 'Accessibility Features (T1015)' } + ), + id: 'T1015', + name: 'Accessibility Features', + reference: 'https://attack.mitre.org/techniques/T1015', + tactics: 'persistence,privilege-escalation', + value: 'accessibilityFeatures', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.accountAccessRemovalDescription', + { defaultMessage: 'Account Access Removal (T1531)' } + ), + id: 'T1531', + name: 'Account Access Removal', + reference: 'https://attack.mitre.org/techniques/T1531', + tactics: 'impact', + value: 'accountAccessRemoval', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.accountDiscoveryDescription', + { defaultMessage: 'Account Discovery (T1087)' } + ), + id: 'T1087', + name: 'Account Discovery', + reference: 'https://attack.mitre.org/techniques/T1087', + tactics: 'discovery', + value: 'accountDiscovery', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.accountManipulationDescription', + { defaultMessage: 'Account Manipulation (T1098)' } + ), + id: 'T1098', + name: 'Account Manipulation', + reference: 'https://attack.mitre.org/techniques/T1098', + tactics: 'credential-access,persistence', + value: 'accountManipulation', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.appCertDlLsDescription', + { defaultMessage: 'AppCert DLLs (T1182)' } + ), + id: 'T1182', + name: 'AppCert DLLs', + reference: 'https://attack.mitre.org/techniques/T1182', + tactics: 'persistence,privilege-escalation', + value: 'appCertDlLs', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.appInitDlLsDescription', + { defaultMessage: 'AppInit DLLs (T1103)' } + ), + id: 'T1103', + name: 'AppInit DLLs', + reference: 'https://attack.mitre.org/techniques/T1103', + tactics: 'persistence,privilege-escalation', + value: 'appInitDlLs', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.appleScriptDescription', + { defaultMessage: 'AppleScript (T1155)' } + ), + id: 'T1155', + name: 'AppleScript', + reference: 'https://attack.mitre.org/techniques/T1155', + tactics: 'execution,lateral-movement', + value: 'appleScript', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.applicationAccessTokenDescription', + { defaultMessage: 'Application Access Token (T1527)' } + ), + id: 'T1527', + name: 'Application Access Token', + reference: 'https://attack.mitre.org/techniques/T1527', + tactics: 'defense-evasion,lateral-movement', + value: 'applicationAccessToken', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.applicationDeploymentSoftwareDescription', + { defaultMessage: 'Application Deployment Software (T1017)' } + ), + id: 'T1017', + name: 'Application Deployment Software', + reference: 'https://attack.mitre.org/techniques/T1017', + tactics: 'lateral-movement', + value: 'applicationDeploymentSoftware', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.applicationShimmingDescription', + { defaultMessage: 'Application Shimming (T1138)' } + ), + id: 'T1138', + name: 'Application Shimming', + reference: 'https://attack.mitre.org/techniques/T1138', + tactics: 'persistence,privilege-escalation', + value: 'applicationShimming', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.applicationWindowDiscoveryDescription', + { defaultMessage: 'Application Window Discovery (T1010)' } + ), + id: 'T1010', + name: 'Application Window Discovery', + reference: 'https://attack.mitre.org/techniques/T1010', + tactics: 'discovery', + value: 'applicationWindowDiscovery', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.audioCaptureDescription', + { defaultMessage: 'Audio Capture (T1123)' } + ), + id: 'T1123', + name: 'Audio Capture', + reference: 'https://attack.mitre.org/techniques/T1123', + tactics: 'collection', + value: 'audioCapture', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.authenticationPackageDescription', + { defaultMessage: 'Authentication Package (T1131)' } + ), + id: 'T1131', + name: 'Authentication Package', + reference: 'https://attack.mitre.org/techniques/T1131', + tactics: 'persistence', + value: 'authenticationPackage', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.automatedCollectionDescription', + { defaultMessage: 'Automated Collection (T1119)' } + ), + id: 'T1119', + name: 'Automated Collection', + reference: 'https://attack.mitre.org/techniques/T1119', + tactics: 'collection', + value: 'automatedCollection', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.automatedExfiltrationDescription', + { defaultMessage: 'Automated Exfiltration (T1020)' } + ), + id: 'T1020', + name: 'Automated Exfiltration', + reference: 'https://attack.mitre.org/techniques/T1020', + tactics: 'exfiltration', + value: 'automatedExfiltration', + }, + { + label: i18n.translate('xpack.siem.detectionEngine.mitreAttackTechniques.bitsJobsDescription', { + defaultMessage: 'BITS Jobs (T1197)', + }), + id: 'T1197', + name: 'BITS Jobs', + reference: 'https://attack.mitre.org/techniques/T1197', + tactics: 'defense-evasion,persistence', + value: 'bitsJobs', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.bashHistoryDescription', + { defaultMessage: 'Bash History (T1139)' } + ), + id: 'T1139', + name: 'Bash History', + reference: 'https://attack.mitre.org/techniques/T1139', + tactics: 'credential-access', + value: 'bashHistory', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.binaryPaddingDescription', + { defaultMessage: 'Binary Padding (T1009)' } + ), + id: 'T1009', + name: 'Binary Padding', + reference: 'https://attack.mitre.org/techniques/T1009', + tactics: 'defense-evasion', + value: 'binaryPadding', + }, + { + label: i18n.translate('xpack.siem.detectionEngine.mitreAttackTechniques.bootkitDescription', { + defaultMessage: 'Bootkit (T1067)', + }), + id: 'T1067', + name: 'Bootkit', + reference: 'https://attack.mitre.org/techniques/T1067', + tactics: 'persistence', + value: 'bootkit', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.browserBookmarkDiscoveryDescription', + { defaultMessage: 'Browser Bookmark Discovery (T1217)' } + ), + id: 'T1217', + name: 'Browser Bookmark Discovery', + reference: 'https://attack.mitre.org/techniques/T1217', + tactics: 'discovery', + value: 'browserBookmarkDiscovery', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.browserExtensionsDescription', + { defaultMessage: 'Browser Extensions (T1176)' } + ), + id: 'T1176', + name: 'Browser Extensions', + reference: 'https://attack.mitre.org/techniques/T1176', + tactics: 'persistence', + value: 'browserExtensions', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.bruteForceDescription', + { defaultMessage: 'Brute Force (T1110)' } + ), + id: 'T1110', + name: 'Brute Force', + reference: 'https://attack.mitre.org/techniques/T1110', + tactics: 'credential-access', + value: 'bruteForce', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.bypassUserAccountControlDescription', + { defaultMessage: 'Bypass User Account Control (T1088)' } + ), + id: 'T1088', + name: 'Bypass User Account Control', + reference: 'https://attack.mitre.org/techniques/T1088', + tactics: 'defense-evasion,privilege-escalation', + value: 'bypassUserAccountControl', + }, + { + label: i18n.translate('xpack.siem.detectionEngine.mitreAttackTechniques.cmstpDescription', { + defaultMessage: 'CMSTP (T1191)', + }), + id: 'T1191', + name: 'CMSTP', + reference: 'https://attack.mitre.org/techniques/T1191', + tactics: 'defense-evasion,execution', + value: 'cmstp', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.changeDefaultFileAssociationDescription', + { defaultMessage: 'Change Default File Association (T1042)' } + ), + id: 'T1042', + name: 'Change Default File Association', + reference: 'https://attack.mitre.org/techniques/T1042', + tactics: 'persistence', + value: 'changeDefaultFileAssociation', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.clearCommandHistoryDescription', + { defaultMessage: 'Clear Command History (T1146)' } + ), + id: 'T1146', + name: 'Clear Command History', + reference: 'https://attack.mitre.org/techniques/T1146', + tactics: 'defense-evasion', + value: 'clearCommandHistory', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.clipboardDataDescription', + { defaultMessage: 'Clipboard Data (T1115)' } + ), + id: 'T1115', + name: 'Clipboard Data', + reference: 'https://attack.mitre.org/techniques/T1115', + tactics: 'collection', + value: 'clipboardData', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.cloudInstanceMetadataApiDescription', + { defaultMessage: 'Cloud Instance Metadata API (T1522)' } + ), + id: 'T1522', + name: 'Cloud Instance Metadata API', + reference: 'https://attack.mitre.org/techniques/T1522', + tactics: 'credential-access', + value: 'cloudInstanceMetadataApi', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.cloudServiceDashboardDescription', + { defaultMessage: 'Cloud Service Dashboard (T1538)' } + ), + id: 'T1538', + name: 'Cloud Service Dashboard', + reference: 'https://attack.mitre.org/techniques/T1538', + tactics: 'discovery', + value: 'cloudServiceDashboard', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.cloudServiceDiscoveryDescription', + { defaultMessage: 'Cloud Service Discovery (T1526)' } + ), + id: 'T1526', + name: 'Cloud Service Discovery', + reference: 'https://attack.mitre.org/techniques/T1526', + tactics: 'discovery', + value: 'cloudServiceDiscovery', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.codeSigningDescription', + { defaultMessage: 'Code Signing (T1116)' } + ), + id: 'T1116', + name: 'Code Signing', + reference: 'https://attack.mitre.org/techniques/T1116', + tactics: 'defense-evasion', + value: 'codeSigning', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.commandLineInterfaceDescription', + { defaultMessage: 'Command-Line Interface (T1059)' } + ), + id: 'T1059', + name: 'Command-Line Interface', + reference: 'https://attack.mitre.org/techniques/T1059', + tactics: 'execution', + value: 'commandLineInterface', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.commonlyUsedPortDescription', + { defaultMessage: 'Commonly Used Port (T1043)' } + ), + id: 'T1043', + name: 'Commonly Used Port', + reference: 'https://attack.mitre.org/techniques/T1043', + tactics: 'command-and-control', + value: 'commonlyUsedPort', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.communicationThroughRemovableMediaDescription', + { defaultMessage: 'Communication Through Removable Media (T1092)' } + ), + id: 'T1092', + name: 'Communication Through Removable Media', + reference: 'https://attack.mitre.org/techniques/T1092', + tactics: 'command-and-control', + value: 'communicationThroughRemovableMedia', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.compileAfterDeliveryDescription', + { defaultMessage: 'Compile After Delivery (T1500)' } + ), + id: 'T1500', + name: 'Compile After Delivery', + reference: 'https://attack.mitre.org/techniques/T1500', + tactics: 'defense-evasion', + value: 'compileAfterDelivery', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.compiledHtmlFileDescription', + { defaultMessage: 'Compiled HTML File (T1223)' } + ), + id: 'T1223', + name: 'Compiled HTML File', + reference: 'https://attack.mitre.org/techniques/T1223', + tactics: 'defense-evasion,execution', + value: 'compiledHtmlFile', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.componentFirmwareDescription', + { defaultMessage: 'Component Firmware (T1109)' } + ), + id: 'T1109', + name: 'Component Firmware', + reference: 'https://attack.mitre.org/techniques/T1109', + tactics: 'defense-evasion,persistence', + value: 'componentFirmware', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.componentObjectModelHijackingDescription', + { defaultMessage: 'Component Object Model Hijacking (T1122)' } + ), + id: 'T1122', + name: 'Component Object Model Hijacking', + reference: 'https://attack.mitre.org/techniques/T1122', + tactics: 'defense-evasion,persistence', + value: 'componentObjectModelHijacking', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.componentObjectModelAndDistributedComDescription', + { defaultMessage: 'Component Object Model and Distributed COM (T1175)' } + ), + id: 'T1175', + name: 'Component Object Model and Distributed COM', + reference: 'https://attack.mitre.org/techniques/T1175', + tactics: 'lateral-movement,execution', + value: 'componentObjectModelAndDistributedCom', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.connectionProxyDescription', + { defaultMessage: 'Connection Proxy (T1090)' } + ), + id: 'T1090', + name: 'Connection Proxy', + reference: 'https://attack.mitre.org/techniques/T1090', + tactics: 'command-and-control,defense-evasion', + value: 'connectionProxy', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.controlPanelItemsDescription', + { defaultMessage: 'Control Panel Items (T1196)' } + ), + id: 'T1196', + name: 'Control Panel Items', + reference: 'https://attack.mitre.org/techniques/T1196', + tactics: 'defense-evasion,execution', + value: 'controlPanelItems', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.createAccountDescription', + { defaultMessage: 'Create Account (T1136)' } + ), + id: 'T1136', + name: 'Create Account', + reference: 'https://attack.mitre.org/techniques/T1136', + tactics: 'persistence', + value: 'createAccount', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.credentialDumpingDescription', + { defaultMessage: 'Credential Dumping (T1003)' } + ), + id: 'T1003', + name: 'Credential Dumping', + reference: 'https://attack.mitre.org/techniques/T1003', + tactics: 'credential-access', + value: 'credentialDumping', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.credentialsFromWebBrowsersDescription', + { defaultMessage: 'Credentials from Web Browsers (T1503)' } + ), + id: 'T1503', + name: 'Credentials from Web Browsers', + reference: 'https://attack.mitre.org/techniques/T1503', + tactics: 'credential-access', + value: 'credentialsFromWebBrowsers', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.credentialsInFilesDescription', + { defaultMessage: 'Credentials in Files (T1081)' } + ), + id: 'T1081', + name: 'Credentials in Files', + reference: 'https://attack.mitre.org/techniques/T1081', + tactics: 'credential-access', + value: 'credentialsInFiles', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.credentialsInRegistryDescription', + { defaultMessage: 'Credentials in Registry (T1214)' } + ), + id: 'T1214', + name: 'Credentials in Registry', + reference: 'https://attack.mitre.org/techniques/T1214', + tactics: 'credential-access', + value: 'credentialsInRegistry', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.customCommandAndControlProtocolDescription', + { defaultMessage: 'Custom Command and Control Protocol (T1094)' } + ), + id: 'T1094', + name: 'Custom Command and Control Protocol', + reference: 'https://attack.mitre.org/techniques/T1094', + tactics: 'command-and-control', + value: 'customCommandAndControlProtocol', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.customCryptographicProtocolDescription', + { defaultMessage: 'Custom Cryptographic Protocol (T1024)' } + ), + id: 'T1024', + name: 'Custom Cryptographic Protocol', + reference: 'https://attack.mitre.org/techniques/T1024', + tactics: 'command-and-control', + value: 'customCryptographicProtocol', + }, + { + label: i18n.translate('xpack.siem.detectionEngine.mitreAttackTechniques.dcShadowDescription', { + defaultMessage: 'DCShadow (T1207)', + }), + id: 'T1207', + name: 'DCShadow', + reference: 'https://attack.mitre.org/techniques/T1207', + tactics: 'defense-evasion', + value: 'dcShadow', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.dllSearchOrderHijackingDescription', + { defaultMessage: 'DLL Search Order Hijacking (T1038)' } + ), + id: 'T1038', + name: 'DLL Search Order Hijacking', + reference: 'https://attack.mitre.org/techniques/T1038', + tactics: 'persistence,privilege-escalation,defense-evasion', + value: 'dllSearchOrderHijacking', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.dllSideLoadingDescription', + { defaultMessage: 'DLL Side-Loading (T1073)' } + ), + id: 'T1073', + name: 'DLL Side-Loading', + reference: 'https://attack.mitre.org/techniques/T1073', + tactics: 'defense-evasion', + value: 'dllSideLoading', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.dataCompressedDescription', + { defaultMessage: 'Data Compressed (T1002)' } + ), + id: 'T1002', + name: 'Data Compressed', + reference: 'https://attack.mitre.org/techniques/T1002', + tactics: 'exfiltration', + value: 'dataCompressed', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.dataDestructionDescription', + { defaultMessage: 'Data Destruction (T1485)' } + ), + id: 'T1485', + name: 'Data Destruction', + reference: 'https://attack.mitre.org/techniques/T1485', + tactics: 'impact', + value: 'dataDestruction', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.dataEncodingDescription', + { defaultMessage: 'Data Encoding (T1132)' } + ), + id: 'T1132', + name: 'Data Encoding', + reference: 'https://attack.mitre.org/techniques/T1132', + tactics: 'command-and-control', + value: 'dataEncoding', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.dataEncryptedDescription', + { defaultMessage: 'Data Encrypted (T1022)' } + ), + id: 'T1022', + name: 'Data Encrypted', + reference: 'https://attack.mitre.org/techniques/T1022', + tactics: 'exfiltration', + value: 'dataEncrypted', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.dataEncryptedForImpactDescription', + { defaultMessage: 'Data Encrypted for Impact (T1486)' } + ), + id: 'T1486', + name: 'Data Encrypted for Impact', + reference: 'https://attack.mitre.org/techniques/T1486', + tactics: 'impact', + value: 'dataEncryptedForImpact', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.dataObfuscationDescription', + { defaultMessage: 'Data Obfuscation (T1001)' } + ), + id: 'T1001', + name: 'Data Obfuscation', + reference: 'https://attack.mitre.org/techniques/T1001', + tactics: 'command-and-control', + value: 'dataObfuscation', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.dataStagedDescription', + { defaultMessage: 'Data Staged (T1074)' } + ), + id: 'T1074', + name: 'Data Staged', + reference: 'https://attack.mitre.org/techniques/T1074', + tactics: 'collection', + value: 'dataStaged', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.dataTransferSizeLimitsDescription', + { defaultMessage: 'Data Transfer Size Limits (T1030)' } + ), + id: 'T1030', + name: 'Data Transfer Size Limits', + reference: 'https://attack.mitre.org/techniques/T1030', + tactics: 'exfiltration', + value: 'dataTransferSizeLimits', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.dataFromCloudStorageObjectDescription', + { defaultMessage: 'Data from Cloud Storage Object (T1530)' } + ), + id: 'T1530', + name: 'Data from Cloud Storage Object', + reference: 'https://attack.mitre.org/techniques/T1530', + tactics: 'collection', + value: 'dataFromCloudStorageObject', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.dataFromInformationRepositoriesDescription', + { defaultMessage: 'Data from Information Repositories (T1213)' } + ), + id: 'T1213', + name: 'Data from Information Repositories', + reference: 'https://attack.mitre.org/techniques/T1213', + tactics: 'collection', + value: 'dataFromInformationRepositories', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.dataFromLocalSystemDescription', + { defaultMessage: 'Data from Local System (T1005)' } + ), + id: 'T1005', + name: 'Data from Local System', + reference: 'https://attack.mitre.org/techniques/T1005', + tactics: 'collection', + value: 'dataFromLocalSystem', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.dataFromNetworkSharedDriveDescription', + { defaultMessage: 'Data from Network Shared Drive (T1039)' } + ), + id: 'T1039', + name: 'Data from Network Shared Drive', + reference: 'https://attack.mitre.org/techniques/T1039', + tactics: 'collection', + value: 'dataFromNetworkSharedDrive', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.dataFromRemovableMediaDescription', + { defaultMessage: 'Data from Removable Media (T1025)' } + ), + id: 'T1025', + name: 'Data from Removable Media', + reference: 'https://attack.mitre.org/techniques/T1025', + tactics: 'collection', + value: 'dataFromRemovableMedia', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.defacementDescription', + { defaultMessage: 'Defacement (T1491)' } + ), + id: 'T1491', + name: 'Defacement', + reference: 'https://attack.mitre.org/techniques/T1491', + tactics: 'impact', + value: 'defacement', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.deobfuscateDecodeFilesOrInformationDescription', + { defaultMessage: 'Deobfuscate/Decode Files or Information (T1140)' } + ), + id: 'T1140', + name: 'Deobfuscate/Decode Files or Information', + reference: 'https://attack.mitre.org/techniques/T1140', + tactics: 'defense-evasion', + value: 'deobfuscateDecodeFilesOrInformation', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.disablingSecurityToolsDescription', + { defaultMessage: 'Disabling Security Tools (T1089)' } + ), + id: 'T1089', + name: 'Disabling Security Tools', + reference: 'https://attack.mitre.org/techniques/T1089', + tactics: 'defense-evasion', + value: 'disablingSecurityTools', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.diskContentWipeDescription', + { defaultMessage: 'Disk Content Wipe (T1488)' } + ), + id: 'T1488', + name: 'Disk Content Wipe', + reference: 'https://attack.mitre.org/techniques/T1488', + tactics: 'impact', + value: 'diskContentWipe', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.diskStructureWipeDescription', + { defaultMessage: 'Disk Structure Wipe (T1487)' } + ), + id: 'T1487', + name: 'Disk Structure Wipe', + reference: 'https://attack.mitre.org/techniques/T1487', + tactics: 'impact', + value: 'diskStructureWipe', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.domainFrontingDescription', + { defaultMessage: 'Domain Fronting (T1172)' } + ), + id: 'T1172', + name: 'Domain Fronting', + reference: 'https://attack.mitre.org/techniques/T1172', + tactics: 'command-and-control', + value: 'domainFronting', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.domainGenerationAlgorithmsDescription', + { defaultMessage: 'Domain Generation Algorithms (T1483)' } + ), + id: 'T1483', + name: 'Domain Generation Algorithms', + reference: 'https://attack.mitre.org/techniques/T1483', + tactics: 'command-and-control', + value: 'domainGenerationAlgorithms', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.domainTrustDiscoveryDescription', + { defaultMessage: 'Domain Trust Discovery (T1482)' } + ), + id: 'T1482', + name: 'Domain Trust Discovery', + reference: 'https://attack.mitre.org/techniques/T1482', + tactics: 'discovery', + value: 'domainTrustDiscovery', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.driveByCompromiseDescription', + { defaultMessage: 'Drive-by Compromise (T1189)' } + ), + id: 'T1189', + name: 'Drive-by Compromise', + reference: 'https://attack.mitre.org/techniques/T1189', + tactics: 'initial-access', + value: 'driveByCompromise', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.dylibHijackingDescription', + { defaultMessage: 'Dylib Hijacking (T1157)' } + ), + id: 'T1157', + name: 'Dylib Hijacking', + reference: 'https://attack.mitre.org/techniques/T1157', + tactics: 'persistence,privilege-escalation', + value: 'dylibHijacking', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.dynamicDataExchangeDescription', + { defaultMessage: 'Dynamic Data Exchange (T1173)' } + ), + id: 'T1173', + name: 'Dynamic Data Exchange', + reference: 'https://attack.mitre.org/techniques/T1173', + tactics: 'execution', + value: 'dynamicDataExchange', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.elevatedExecutionWithPromptDescription', + { defaultMessage: 'Elevated Execution with Prompt (T1514)' } + ), + id: 'T1514', + name: 'Elevated Execution with Prompt', + reference: 'https://attack.mitre.org/techniques/T1514', + tactics: 'privilege-escalation', + value: 'elevatedExecutionWithPrompt', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.emailCollectionDescription', + { defaultMessage: 'Email Collection (T1114)' } + ), + id: 'T1114', + name: 'Email Collection', + reference: 'https://attack.mitre.org/techniques/T1114', + tactics: 'collection', + value: 'emailCollection', + }, + { + label: i18n.translate('xpack.siem.detectionEngine.mitreAttackTechniques.emondDescription', { + defaultMessage: 'Emond (T1519)', + }), + id: 'T1519', + name: 'Emond', + reference: 'https://attack.mitre.org/techniques/T1519', + tactics: 'persistence,privilege-escalation', + value: 'emond', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.endpointDenialOfServiceDescription', + { defaultMessage: 'Endpoint Denial of Service (T1499)' } + ), + id: 'T1499', + name: 'Endpoint Denial of Service', + reference: 'https://attack.mitre.org/techniques/T1499', + tactics: 'impact', + value: 'endpointDenialOfService', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.executionGuardrailsDescription', + { defaultMessage: 'Execution Guardrails (T1480)' } + ), + id: 'T1480', + name: 'Execution Guardrails', + reference: 'https://attack.mitre.org/techniques/T1480', + tactics: 'defense-evasion', + value: 'executionGuardrails', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.executionThroughApiDescription', + { defaultMessage: 'Execution through API (T1106)' } + ), + id: 'T1106', + name: 'Execution through API', + reference: 'https://attack.mitre.org/techniques/T1106', + tactics: 'execution', + value: 'executionThroughApi', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.executionThroughModuleLoadDescription', + { defaultMessage: 'Execution through Module Load (T1129)' } + ), + id: 'T1129', + name: 'Execution through Module Load', + reference: 'https://attack.mitre.org/techniques/T1129', + tactics: 'execution', + value: 'executionThroughModuleLoad', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.exfiltrationOverAlternativeProtocolDescription', + { defaultMessage: 'Exfiltration Over Alternative Protocol (T1048)' } + ), + id: 'T1048', + name: 'Exfiltration Over Alternative Protocol', + reference: 'https://attack.mitre.org/techniques/T1048', + tactics: 'exfiltration', + value: 'exfiltrationOverAlternativeProtocol', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.exfiltrationOverCommandAndControlChannelDescription', + { defaultMessage: 'Exfiltration Over Command and Control Channel (T1041)' } + ), + id: 'T1041', + name: 'Exfiltration Over Command and Control Channel', + reference: 'https://attack.mitre.org/techniques/T1041', + tactics: 'exfiltration', + value: 'exfiltrationOverCommandAndControlChannel', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.exfiltrationOverOtherNetworkMediumDescription', + { defaultMessage: 'Exfiltration Over Other Network Medium (T1011)' } + ), + id: 'T1011', + name: 'Exfiltration Over Other Network Medium', + reference: 'https://attack.mitre.org/techniques/T1011', + tactics: 'exfiltration', + value: 'exfiltrationOverOtherNetworkMedium', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.exfiltrationOverPhysicalMediumDescription', + { defaultMessage: 'Exfiltration Over Physical Medium (T1052)' } + ), + id: 'T1052', + name: 'Exfiltration Over Physical Medium', + reference: 'https://attack.mitre.org/techniques/T1052', + tactics: 'exfiltration', + value: 'exfiltrationOverPhysicalMedium', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.exploitPublicFacingApplicationDescription', + { defaultMessage: 'Exploit Public-Facing Application (T1190)' } + ), + id: 'T1190', + name: 'Exploit Public-Facing Application', + reference: 'https://attack.mitre.org/techniques/T1190', + tactics: 'initial-access', + value: 'exploitPublicFacingApplication', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.exploitationForClientExecutionDescription', + { defaultMessage: 'Exploitation for Client Execution (T1203)' } + ), + id: 'T1203', + name: 'Exploitation for Client Execution', + reference: 'https://attack.mitre.org/techniques/T1203', + tactics: 'execution', + value: 'exploitationForClientExecution', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.exploitationForCredentialAccessDescription', + { defaultMessage: 'Exploitation for Credential Access (T1212)' } + ), + id: 'T1212', + name: 'Exploitation for Credential Access', + reference: 'https://attack.mitre.org/techniques/T1212', + tactics: 'credential-access', + value: 'exploitationForCredentialAccess', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.exploitationForDefenseEvasionDescription', + { defaultMessage: 'Exploitation for Defense Evasion (T1211)' } + ), + id: 'T1211', + name: 'Exploitation for Defense Evasion', + reference: 'https://attack.mitre.org/techniques/T1211', + tactics: 'defense-evasion', + value: 'exploitationForDefenseEvasion', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.exploitationForPrivilegeEscalationDescription', + { defaultMessage: 'Exploitation for Privilege Escalation (T1068)' } + ), + id: 'T1068', + name: 'Exploitation for Privilege Escalation', + reference: 'https://attack.mitre.org/techniques/T1068', + tactics: 'privilege-escalation', + value: 'exploitationForPrivilegeEscalation', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.exploitationOfRemoteServicesDescription', + { defaultMessage: 'Exploitation of Remote Services (T1210)' } + ), + id: 'T1210', + name: 'Exploitation of Remote Services', + reference: 'https://attack.mitre.org/techniques/T1210', + tactics: 'lateral-movement', + value: 'exploitationOfRemoteServices', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.externalRemoteServicesDescription', + { defaultMessage: 'External Remote Services (T1133)' } + ), + id: 'T1133', + name: 'External Remote Services', + reference: 'https://attack.mitre.org/techniques/T1133', + tactics: 'persistence,initial-access', + value: 'externalRemoteServices', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.extraWindowMemoryInjectionDescription', + { defaultMessage: 'Extra Window Memory Injection (T1181)' } + ), + id: 'T1181', + name: 'Extra Window Memory Injection', + reference: 'https://attack.mitre.org/techniques/T1181', + tactics: 'defense-evasion,privilege-escalation', + value: 'extraWindowMemoryInjection', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.fallbackChannelsDescription', + { defaultMessage: 'Fallback Channels (T1008)' } + ), + id: 'T1008', + name: 'Fallback Channels', + reference: 'https://attack.mitre.org/techniques/T1008', + tactics: 'command-and-control', + value: 'fallbackChannels', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.fileDeletionDescription', + { defaultMessage: 'File Deletion (T1107)' } + ), + id: 'T1107', + name: 'File Deletion', + reference: 'https://attack.mitre.org/techniques/T1107', + tactics: 'defense-evasion', + value: 'fileDeletion', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.fileSystemLogicalOffsetsDescription', + { defaultMessage: 'File System Logical Offsets (T1006)' } + ), + id: 'T1006', + name: 'File System Logical Offsets', + reference: 'https://attack.mitre.org/techniques/T1006', + tactics: 'defense-evasion', + value: 'fileSystemLogicalOffsets', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.fileSystemPermissionsWeaknessDescription', + { defaultMessage: 'File System Permissions Weakness (T1044)' } + ), + id: 'T1044', + name: 'File System Permissions Weakness', + reference: 'https://attack.mitre.org/techniques/T1044', + tactics: 'persistence,privilege-escalation', + value: 'fileSystemPermissionsWeakness', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.fileAndDirectoryDiscoveryDescription', + { defaultMessage: 'File and Directory Discovery (T1083)' } + ), + id: 'T1083', + name: 'File and Directory Discovery', + reference: 'https://attack.mitre.org/techniques/T1083', + tactics: 'discovery', + value: 'fileAndDirectoryDiscovery', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.fileAndDirectoryPermissionsModificationDescription', + { defaultMessage: 'File and Directory Permissions Modification (T1222)' } + ), + id: 'T1222', + name: 'File and Directory Permissions Modification', + reference: 'https://attack.mitre.org/techniques/T1222', + tactics: 'defense-evasion', + value: 'fileAndDirectoryPermissionsModification', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.firmwareCorruptionDescription', + { defaultMessage: 'Firmware Corruption (T1495)' } + ), + id: 'T1495', + name: 'Firmware Corruption', + reference: 'https://attack.mitre.org/techniques/T1495', + tactics: 'impact', + value: 'firmwareCorruption', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.forcedAuthenticationDescription', + { defaultMessage: 'Forced Authentication (T1187)' } + ), + id: 'T1187', + name: 'Forced Authentication', + reference: 'https://attack.mitre.org/techniques/T1187', + tactics: 'credential-access', + value: 'forcedAuthentication', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.gatekeeperBypassDescription', + { defaultMessage: 'Gatekeeper Bypass (T1144)' } + ), + id: 'T1144', + name: 'Gatekeeper Bypass', + reference: 'https://attack.mitre.org/techniques/T1144', + tactics: 'defense-evasion', + value: 'gatekeeperBypass', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.graphicalUserInterfaceDescription', + { defaultMessage: 'Graphical User Interface (T1061)' } + ), + id: 'T1061', + name: 'Graphical User Interface', + reference: 'https://attack.mitre.org/techniques/T1061', + tactics: 'execution', + value: 'graphicalUserInterface', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.groupPolicyModificationDescription', + { defaultMessage: 'Group Policy Modification (T1484)' } + ), + id: 'T1484', + name: 'Group Policy Modification', + reference: 'https://attack.mitre.org/techniques/T1484', + tactics: 'defense-evasion', + value: 'groupPolicyModification', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.histcontrolDescription', + { defaultMessage: 'HISTCONTROL (T1148)' } + ), + id: 'T1148', + name: 'HISTCONTROL', + reference: 'https://attack.mitre.org/techniques/T1148', + tactics: 'defense-evasion', + value: 'histcontrol', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.hardwareAdditionsDescription', + { defaultMessage: 'Hardware Additions (T1200)' } + ), + id: 'T1200', + name: 'Hardware Additions', + reference: 'https://attack.mitre.org/techniques/T1200', + tactics: 'initial-access', + value: 'hardwareAdditions', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.hiddenFilesAndDirectoriesDescription', + { defaultMessage: 'Hidden Files and Directories (T1158)' } + ), + id: 'T1158', + name: 'Hidden Files and Directories', + reference: 'https://attack.mitre.org/techniques/T1158', + tactics: 'defense-evasion,persistence', + value: 'hiddenFilesAndDirectories', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.hiddenUsersDescription', + { defaultMessage: 'Hidden Users (T1147)' } + ), + id: 'T1147', + name: 'Hidden Users', + reference: 'https://attack.mitre.org/techniques/T1147', + tactics: 'defense-evasion', + value: 'hiddenUsers', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.hiddenWindowDescription', + { defaultMessage: 'Hidden Window (T1143)' } + ), + id: 'T1143', + name: 'Hidden Window', + reference: 'https://attack.mitre.org/techniques/T1143', + tactics: 'defense-evasion', + value: 'hiddenWindow', + }, + { + label: i18n.translate('xpack.siem.detectionEngine.mitreAttackTechniques.hookingDescription', { + defaultMessage: 'Hooking (T1179)', + }), + id: 'T1179', + name: 'Hooking', + reference: 'https://attack.mitre.org/techniques/T1179', + tactics: 'persistence,privilege-escalation,credential-access', + value: 'hooking', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.hypervisorDescription', + { defaultMessage: 'Hypervisor (T1062)' } + ), + id: 'T1062', + name: 'Hypervisor', + reference: 'https://attack.mitre.org/techniques/T1062', + tactics: 'persistence', + value: 'hypervisor', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.imageFileExecutionOptionsInjectionDescription', + { defaultMessage: 'Image File Execution Options Injection (T1183)' } + ), + id: 'T1183', + name: 'Image File Execution Options Injection', + reference: 'https://attack.mitre.org/techniques/T1183', + tactics: 'privilege-escalation,persistence,defense-evasion', + value: 'imageFileExecutionOptionsInjection', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.implantContainerImageDescription', + { defaultMessage: 'Implant Container Image (T1525)' } + ), + id: 'T1525', + name: 'Implant Container Image', + reference: 'https://attack.mitre.org/techniques/T1525', + tactics: 'persistence', + value: 'implantContainerImage', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.indicatorBlockingDescription', + { defaultMessage: 'Indicator Blocking (T1054)' } + ), + id: 'T1054', + name: 'Indicator Blocking', + reference: 'https://attack.mitre.org/techniques/T1054', + tactics: 'defense-evasion', + value: 'indicatorBlocking', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.indicatorRemovalFromToolsDescription', + { defaultMessage: 'Indicator Removal from Tools (T1066)' } + ), + id: 'T1066', + name: 'Indicator Removal from Tools', + reference: 'https://attack.mitre.org/techniques/T1066', + tactics: 'defense-evasion', + value: 'indicatorRemovalFromTools', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.indicatorRemovalOnHostDescription', + { defaultMessage: 'Indicator Removal on Host (T1070)' } + ), + id: 'T1070', + name: 'Indicator Removal on Host', + reference: 'https://attack.mitre.org/techniques/T1070', + tactics: 'defense-evasion', + value: 'indicatorRemovalOnHost', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.indirectCommandExecutionDescription', + { defaultMessage: 'Indirect Command Execution (T1202)' } + ), + id: 'T1202', + name: 'Indirect Command Execution', + reference: 'https://attack.mitre.org/techniques/T1202', + tactics: 'defense-evasion', + value: 'indirectCommandExecution', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.inhibitSystemRecoveryDescription', + { defaultMessage: 'Inhibit System Recovery (T1490)' } + ), + id: 'T1490', + name: 'Inhibit System Recovery', + reference: 'https://attack.mitre.org/techniques/T1490', + tactics: 'impact', + value: 'inhibitSystemRecovery', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.inputCaptureDescription', + { defaultMessage: 'Input Capture (T1056)' } + ), + id: 'T1056', + name: 'Input Capture', + reference: 'https://attack.mitre.org/techniques/T1056', + tactics: 'collection,credential-access', + value: 'inputCapture', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.inputPromptDescription', + { defaultMessage: 'Input Prompt (T1141)' } + ), + id: 'T1141', + name: 'Input Prompt', + reference: 'https://attack.mitre.org/techniques/T1141', + tactics: 'credential-access', + value: 'inputPrompt', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.installRootCertificateDescription', + { defaultMessage: 'Install Root Certificate (T1130)' } + ), + id: 'T1130', + name: 'Install Root Certificate', + reference: 'https://attack.mitre.org/techniques/T1130', + tactics: 'defense-evasion', + value: 'installRootCertificate', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.installUtilDescription', + { defaultMessage: 'InstallUtil (T1118)' } + ), + id: 'T1118', + name: 'InstallUtil', + reference: 'https://attack.mitre.org/techniques/T1118', + tactics: 'defense-evasion,execution', + value: 'installUtil', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.internalSpearphishingDescription', + { defaultMessage: 'Internal Spearphishing (T1534)' } + ), + id: 'T1534', + name: 'Internal Spearphishing', + reference: 'https://attack.mitre.org/techniques/T1534', + tactics: 'lateral-movement', + value: 'internalSpearphishing', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.kerberoastingDescription', + { defaultMessage: 'Kerberoasting (T1208)' } + ), + id: 'T1208', + name: 'Kerberoasting', + reference: 'https://attack.mitre.org/techniques/T1208', + tactics: 'credential-access', + value: 'kerberoasting', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.kernelModulesAndExtensionsDescription', + { defaultMessage: 'Kernel Modules and Extensions (T1215)' } + ), + id: 'T1215', + name: 'Kernel Modules and Extensions', + reference: 'https://attack.mitre.org/techniques/T1215', + tactics: 'persistence', + value: 'kernelModulesAndExtensions', + }, + { + label: i18n.translate('xpack.siem.detectionEngine.mitreAttackTechniques.keychainDescription', { + defaultMessage: 'Keychain (T1142)', + }), + id: 'T1142', + name: 'Keychain', + reference: 'https://attack.mitre.org/techniques/T1142', + tactics: 'credential-access', + value: 'keychain', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.lcLoadDylibAdditionDescription', + { defaultMessage: 'LC_LOAD_DYLIB Addition (T1161)' } + ), + id: 'T1161', + name: 'LC_LOAD_DYLIB Addition', + reference: 'https://attack.mitre.org/techniques/T1161', + tactics: 'persistence', + value: 'lcLoadDylibAddition', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.lcMainHijackingDescription', + { defaultMessage: 'LC_MAIN Hijacking (T1149)' } + ), + id: 'T1149', + name: 'LC_MAIN Hijacking', + reference: 'https://attack.mitre.org/techniques/T1149', + tactics: 'defense-evasion', + value: 'lcMainHijacking', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.llmnrNbtNsPoisoningAndRelayDescription', + { defaultMessage: 'LLMNR/NBT-NS Poisoning and Relay (T1171)' } + ), + id: 'T1171', + name: 'LLMNR/NBT-NS Poisoning and Relay', + reference: 'https://attack.mitre.org/techniques/T1171', + tactics: 'credential-access', + value: 'llmnrNbtNsPoisoningAndRelay', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.lsassDriverDescription', + { defaultMessage: 'LSASS Driver (T1177)' } + ), + id: 'T1177', + name: 'LSASS Driver', + reference: 'https://attack.mitre.org/techniques/T1177', + tactics: 'execution,persistence', + value: 'lsassDriver', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.launchAgentDescription', + { defaultMessage: 'Launch Agent (T1159)' } + ), + id: 'T1159', + name: 'Launch Agent', + reference: 'https://attack.mitre.org/techniques/T1159', + tactics: 'persistence', + value: 'launchAgent', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.launchDaemonDescription', + { defaultMessage: 'Launch Daemon (T1160)' } + ), + id: 'T1160', + name: 'Launch Daemon', + reference: 'https://attack.mitre.org/techniques/T1160', + tactics: 'persistence,privilege-escalation', + value: 'launchDaemon', + }, + { + label: i18n.translate('xpack.siem.detectionEngine.mitreAttackTechniques.launchctlDescription', { + defaultMessage: 'Launchctl (T1152)', + }), + id: 'T1152', + name: 'Launchctl', + reference: 'https://attack.mitre.org/techniques/T1152', + tactics: 'defense-evasion,execution,persistence', + value: 'launchctl', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.localJobSchedulingDescription', + { defaultMessage: 'Local Job Scheduling (T1168)' } + ), + id: 'T1168', + name: 'Local Job Scheduling', + reference: 'https://attack.mitre.org/techniques/T1168', + tactics: 'persistence,execution', + value: 'localJobScheduling', + }, + { + label: i18n.translate('xpack.siem.detectionEngine.mitreAttackTechniques.loginItemDescription', { + defaultMessage: 'Login Item (T1162)', + }), + id: 'T1162', + name: 'Login Item', + reference: 'https://attack.mitre.org/techniques/T1162', + tactics: 'persistence', + value: 'loginItem', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.logonScriptsDescription', + { defaultMessage: 'Logon Scripts (T1037)' } + ), + id: 'T1037', + name: 'Logon Scripts', + reference: 'https://attack.mitre.org/techniques/T1037', + tactics: 'lateral-movement,persistence', + value: 'logonScripts', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.manInTheBrowserDescription', + { defaultMessage: 'Man in the Browser (T1185)' } + ), + id: 'T1185', + name: 'Man in the Browser', + reference: 'https://attack.mitre.org/techniques/T1185', + tactics: 'collection', + value: 'manInTheBrowser', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.masqueradingDescription', + { defaultMessage: 'Masquerading (T1036)' } + ), + id: 'T1036', + name: 'Masquerading', + reference: 'https://attack.mitre.org/techniques/T1036', + tactics: 'defense-evasion', + value: 'masquerading', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.modifyExistingServiceDescription', + { defaultMessage: 'Modify Existing Service (T1031)' } + ), + id: 'T1031', + name: 'Modify Existing Service', + reference: 'https://attack.mitre.org/techniques/T1031', + tactics: 'persistence', + value: 'modifyExistingService', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.modifyRegistryDescription', + { defaultMessage: 'Modify Registry (T1112)' } + ), + id: 'T1112', + name: 'Modify Registry', + reference: 'https://attack.mitre.org/techniques/T1112', + tactics: 'defense-evasion', + value: 'modifyRegistry', + }, + { + label: i18n.translate('xpack.siem.detectionEngine.mitreAttackTechniques.mshtaDescription', { + defaultMessage: 'Mshta (T1170)', + }), + id: 'T1170', + name: 'Mshta', + reference: 'https://attack.mitre.org/techniques/T1170', + tactics: 'defense-evasion,execution', + value: 'mshta', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.multiStageChannelsDescription', + { defaultMessage: 'Multi-Stage Channels (T1104)' } + ), + id: 'T1104', + name: 'Multi-Stage Channels', + reference: 'https://attack.mitre.org/techniques/T1104', + tactics: 'command-and-control', + value: 'multiStageChannels', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.multiHopProxyDescription', + { defaultMessage: 'Multi-hop Proxy (T1188)' } + ), + id: 'T1188', + name: 'Multi-hop Proxy', + reference: 'https://attack.mitre.org/techniques/T1188', + tactics: 'command-and-control', + value: 'multiHopProxy', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.multibandCommunicationDescription', + { defaultMessage: 'Multiband Communication (T1026)' } + ), + id: 'T1026', + name: 'Multiband Communication', + reference: 'https://attack.mitre.org/techniques/T1026', + tactics: 'command-and-control', + value: 'multibandCommunication', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.multilayerEncryptionDescription', + { defaultMessage: 'Multilayer Encryption (T1079)' } + ), + id: 'T1079', + name: 'Multilayer Encryption', + reference: 'https://attack.mitre.org/techniques/T1079', + tactics: 'command-and-control', + value: 'multilayerEncryption', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.ntfsFileAttributesDescription', + { defaultMessage: 'NTFS File Attributes (T1096)' } + ), + id: 'T1096', + name: 'NTFS File Attributes', + reference: 'https://attack.mitre.org/techniques/T1096', + tactics: 'defense-evasion', + value: 'ntfsFileAttributes', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.netshHelperDllDescription', + { defaultMessage: 'Netsh Helper DLL (T1128)' } + ), + id: 'T1128', + name: 'Netsh Helper DLL', + reference: 'https://attack.mitre.org/techniques/T1128', + tactics: 'persistence', + value: 'netshHelperDll', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.networkDenialOfServiceDescription', + { defaultMessage: 'Network Denial of Service (T1498)' } + ), + id: 'T1498', + name: 'Network Denial of Service', + reference: 'https://attack.mitre.org/techniques/T1498', + tactics: 'impact', + value: 'networkDenialOfService', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.networkServiceScanningDescription', + { defaultMessage: 'Network Service Scanning (T1046)' } + ), + id: 'T1046', + name: 'Network Service Scanning', + reference: 'https://attack.mitre.org/techniques/T1046', + tactics: 'discovery', + value: 'networkServiceScanning', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.networkShareConnectionRemovalDescription', + { defaultMessage: 'Network Share Connection Removal (T1126)' } + ), + id: 'T1126', + name: 'Network Share Connection Removal', + reference: 'https://attack.mitre.org/techniques/T1126', + tactics: 'defense-evasion', + value: 'networkShareConnectionRemoval', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.networkShareDiscoveryDescription', + { defaultMessage: 'Network Share Discovery (T1135)' } + ), + id: 'T1135', + name: 'Network Share Discovery', + reference: 'https://attack.mitre.org/techniques/T1135', + tactics: 'discovery', + value: 'networkShareDiscovery', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.networkSniffingDescription', + { defaultMessage: 'Network Sniffing (T1040)' } + ), + id: 'T1040', + name: 'Network Sniffing', + reference: 'https://attack.mitre.org/techniques/T1040', + tactics: 'credential-access,discovery', + value: 'networkSniffing', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.newServiceDescription', + { defaultMessage: 'New Service (T1050)' } + ), + id: 'T1050', + name: 'New Service', + reference: 'https://attack.mitre.org/techniques/T1050', + tactics: 'persistence,privilege-escalation', + value: 'newService', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.obfuscatedFilesOrInformationDescription', + { defaultMessage: 'Obfuscated Files or Information (T1027)' } + ), + id: 'T1027', + name: 'Obfuscated Files or Information', + reference: 'https://attack.mitre.org/techniques/T1027', + tactics: 'defense-evasion', + value: 'obfuscatedFilesOrInformation', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.officeApplicationStartupDescription', + { defaultMessage: 'Office Application Startup (T1137)' } + ), + id: 'T1137', + name: 'Office Application Startup', + reference: 'https://attack.mitre.org/techniques/T1137', + tactics: 'persistence', + value: 'officeApplicationStartup', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.parentPidSpoofingDescription', + { defaultMessage: 'Parent PID Spoofing (T1502)' } + ), + id: 'T1502', + name: 'Parent PID Spoofing', + reference: 'https://attack.mitre.org/techniques/T1502', + tactics: 'defense-evasion,privilege-escalation', + value: 'parentPidSpoofing', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.passTheHashDescription', + { defaultMessage: 'Pass the Hash (T1075)' } + ), + id: 'T1075', + name: 'Pass the Hash', + reference: 'https://attack.mitre.org/techniques/T1075', + tactics: 'lateral-movement', + value: 'passTheHash', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.passTheTicketDescription', + { defaultMessage: 'Pass the Ticket (T1097)' } + ), + id: 'T1097', + name: 'Pass the Ticket', + reference: 'https://attack.mitre.org/techniques/T1097', + tactics: 'lateral-movement', + value: 'passTheTicket', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.passwordFilterDllDescription', + { defaultMessage: 'Password Filter DLL (T1174)' } + ), + id: 'T1174', + name: 'Password Filter DLL', + reference: 'https://attack.mitre.org/techniques/T1174', + tactics: 'credential-access', + value: 'passwordFilterDll', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.passwordPolicyDiscoveryDescription', + { defaultMessage: 'Password Policy Discovery (T1201)' } + ), + id: 'T1201', + name: 'Password Policy Discovery', + reference: 'https://attack.mitre.org/techniques/T1201', + tactics: 'discovery', + value: 'passwordPolicyDiscovery', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.pathInterceptionDescription', + { defaultMessage: 'Path Interception (T1034)' } + ), + id: 'T1034', + name: 'Path Interception', + reference: 'https://attack.mitre.org/techniques/T1034', + tactics: 'persistence,privilege-escalation', + value: 'pathInterception', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.peripheralDeviceDiscoveryDescription', + { defaultMessage: 'Peripheral Device Discovery (T1120)' } + ), + id: 'T1120', + name: 'Peripheral Device Discovery', + reference: 'https://attack.mitre.org/techniques/T1120', + tactics: 'discovery', + value: 'peripheralDeviceDiscovery', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.permissionGroupsDiscoveryDescription', + { defaultMessage: 'Permission Groups Discovery (T1069)' } + ), + id: 'T1069', + name: 'Permission Groups Discovery', + reference: 'https://attack.mitre.org/techniques/T1069', + tactics: 'discovery', + value: 'permissionGroupsDiscovery', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.plistModificationDescription', + { defaultMessage: 'Plist Modification (T1150)' } + ), + id: 'T1150', + name: 'Plist Modification', + reference: 'https://attack.mitre.org/techniques/T1150', + tactics: 'defense-evasion,persistence,privilege-escalation', + value: 'plistModification', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.portKnockingDescription', + { defaultMessage: 'Port Knocking (T1205)' } + ), + id: 'T1205', + name: 'Port Knocking', + reference: 'https://attack.mitre.org/techniques/T1205', + tactics: 'defense-evasion,persistence,command-and-control', + value: 'portKnocking', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.portMonitorsDescription', + { defaultMessage: 'Port Monitors (T1013)' } + ), + id: 'T1013', + name: 'Port Monitors', + reference: 'https://attack.mitre.org/techniques/T1013', + tactics: 'persistence,privilege-escalation', + value: 'portMonitors', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.powerShellDescription', + { defaultMessage: 'PowerShell (T1086)' } + ), + id: 'T1086', + name: 'PowerShell', + reference: 'https://attack.mitre.org/techniques/T1086', + tactics: 'execution', + value: 'powerShell', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.powerShellProfileDescription', + { defaultMessage: 'PowerShell Profile (T1504)' } + ), + id: 'T1504', + name: 'PowerShell Profile', + reference: 'https://attack.mitre.org/techniques/T1504', + tactics: 'persistence,privilege-escalation', + value: 'powerShellProfile', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.privateKeysDescription', + { defaultMessage: 'Private Keys (T1145)' } + ), + id: 'T1145', + name: 'Private Keys', + reference: 'https://attack.mitre.org/techniques/T1145', + tactics: 'credential-access', + value: 'privateKeys', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.processDiscoveryDescription', + { defaultMessage: 'Process Discovery (T1057)' } + ), + id: 'T1057', + name: 'Process Discovery', + reference: 'https://attack.mitre.org/techniques/T1057', + tactics: 'discovery', + value: 'processDiscovery', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.processDoppelgangingDescription', + { defaultMessage: 'Process Doppelgänging (T1186)' } + ), + id: 'T1186', + name: 'Process Doppelgänging', + reference: 'https://attack.mitre.org/techniques/T1186', + tactics: 'defense-evasion', + value: 'processDoppelganging', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.processHollowingDescription', + { defaultMessage: 'Process Hollowing (T1093)' } + ), + id: 'T1093', + name: 'Process Hollowing', + reference: 'https://attack.mitre.org/techniques/T1093', + tactics: 'defense-evasion', + value: 'processHollowing', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.processInjectionDescription', + { defaultMessage: 'Process Injection (T1055)' } + ), + id: 'T1055', + name: 'Process Injection', + reference: 'https://attack.mitre.org/techniques/T1055', + tactics: 'defense-evasion,privilege-escalation', + value: 'processInjection', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.queryRegistryDescription', + { defaultMessage: 'Query Registry (T1012)' } + ), + id: 'T1012', + name: 'Query Registry', + reference: 'https://attack.mitre.org/techniques/T1012', + tactics: 'discovery', + value: 'queryRegistry', + }, + { + label: i18n.translate('xpack.siem.detectionEngine.mitreAttackTechniques.rcCommonDescription', { + defaultMessage: 'Rc.common (T1163)', + }), + id: 'T1163', + name: 'Rc.common', + reference: 'https://attack.mitre.org/techniques/T1163', + tactics: 'persistence', + value: 'rcCommon', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.reOpenedApplicationsDescription', + { defaultMessage: 'Re-opened Applications (T1164)' } + ), + id: 'T1164', + name: 'Re-opened Applications', + reference: 'https://attack.mitre.org/techniques/T1164', + tactics: 'persistence', + value: 'reOpenedApplications', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.redundantAccessDescription', + { defaultMessage: 'Redundant Access (T1108)' } + ), + id: 'T1108', + name: 'Redundant Access', + reference: 'https://attack.mitre.org/techniques/T1108', + tactics: 'defense-evasion,persistence', + value: 'redundantAccess', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.registryRunKeysStartupFolderDescription', + { defaultMessage: 'Registry Run Keys / Startup Folder (T1060)' } + ), + id: 'T1060', + name: 'Registry Run Keys / Startup Folder', + reference: 'https://attack.mitre.org/techniques/T1060', + tactics: 'persistence', + value: 'registryRunKeysStartupFolder', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.regsvcsRegasmDescription', + { defaultMessage: 'Regsvcs/Regasm (T1121)' } + ), + id: 'T1121', + name: 'Regsvcs/Regasm', + reference: 'https://attack.mitre.org/techniques/T1121', + tactics: 'defense-evasion,execution', + value: 'regsvcsRegasm', + }, + { + label: i18n.translate('xpack.siem.detectionEngine.mitreAttackTechniques.regsvr32Description', { + defaultMessage: 'Regsvr32 (T1117)', + }), + id: 'T1117', + name: 'Regsvr32', + reference: 'https://attack.mitre.org/techniques/T1117', + tactics: 'defense-evasion,execution', + value: 'regsvr32', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.remoteAccessToolsDescription', + { defaultMessage: 'Remote Access Tools (T1219)' } + ), + id: 'T1219', + name: 'Remote Access Tools', + reference: 'https://attack.mitre.org/techniques/T1219', + tactics: 'command-and-control', + value: 'remoteAccessTools', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.remoteDesktopProtocolDescription', + { defaultMessage: 'Remote Desktop Protocol (T1076)' } + ), + id: 'T1076', + name: 'Remote Desktop Protocol', + reference: 'https://attack.mitre.org/techniques/T1076', + tactics: 'lateral-movement', + value: 'remoteDesktopProtocol', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.remoteFileCopyDescription', + { defaultMessage: 'Remote File Copy (T1105)' } + ), + id: 'T1105', + name: 'Remote File Copy', + reference: 'https://attack.mitre.org/techniques/T1105', + tactics: 'command-and-control,lateral-movement', + value: 'remoteFileCopy', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.remoteServicesDescription', + { defaultMessage: 'Remote Services (T1021)' } + ), + id: 'T1021', + name: 'Remote Services', + reference: 'https://attack.mitre.org/techniques/T1021', + tactics: 'lateral-movement', + value: 'remoteServices', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.remoteSystemDiscoveryDescription', + { defaultMessage: 'Remote System Discovery (T1018)' } + ), + id: 'T1018', + name: 'Remote System Discovery', + reference: 'https://attack.mitre.org/techniques/T1018', + tactics: 'discovery', + value: 'remoteSystemDiscovery', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.replicationThroughRemovableMediaDescription', + { defaultMessage: 'Replication Through Removable Media (T1091)' } + ), + id: 'T1091', + name: 'Replication Through Removable Media', + reference: 'https://attack.mitre.org/techniques/T1091', + tactics: 'lateral-movement,initial-access', + value: 'replicationThroughRemovableMedia', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.resourceHijackingDescription', + { defaultMessage: 'Resource Hijacking (T1496)' } + ), + id: 'T1496', + name: 'Resource Hijacking', + reference: 'https://attack.mitre.org/techniques/T1496', + tactics: 'impact', + value: 'resourceHijacking', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.revertCloudInstanceDescription', + { defaultMessage: 'Revert Cloud Instance (T1536)' } + ), + id: 'T1536', + name: 'Revert Cloud Instance', + reference: 'https://attack.mitre.org/techniques/T1536', + tactics: 'defense-evasion', + value: 'revertCloudInstance', + }, + { + label: i18n.translate('xpack.siem.detectionEngine.mitreAttackTechniques.rootkitDescription', { + defaultMessage: 'Rootkit (T1014)', + }), + id: 'T1014', + name: 'Rootkit', + reference: 'https://attack.mitre.org/techniques/T1014', + tactics: 'defense-evasion', + value: 'rootkit', + }, + { + label: i18n.translate('xpack.siem.detectionEngine.mitreAttackTechniques.rundll32Description', { + defaultMessage: 'Rundll32 (T1085)', + }), + id: 'T1085', + name: 'Rundll32', + reference: 'https://attack.mitre.org/techniques/T1085', + tactics: 'defense-evasion,execution', + value: 'rundll32', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.runtimeDataManipulationDescription', + { defaultMessage: 'Runtime Data Manipulation (T1494)' } + ), + id: 'T1494', + name: 'Runtime Data Manipulation', + reference: 'https://attack.mitre.org/techniques/T1494', + tactics: 'impact', + value: 'runtimeDataManipulation', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.sidHistoryInjectionDescription', + { defaultMessage: 'SID-History Injection (T1178)' } + ), + id: 'T1178', + name: 'SID-History Injection', + reference: 'https://attack.mitre.org/techniques/T1178', + tactics: 'privilege-escalation', + value: 'sidHistoryInjection', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.sipAndTrustProviderHijackingDescription', + { defaultMessage: 'SIP and Trust Provider Hijacking (T1198)' } + ), + id: 'T1198', + name: 'SIP and Trust Provider Hijacking', + reference: 'https://attack.mitre.org/techniques/T1198', + tactics: 'defense-evasion,persistence', + value: 'sipAndTrustProviderHijacking', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.sshHijackingDescription', + { defaultMessage: 'SSH Hijacking (T1184)' } + ), + id: 'T1184', + name: 'SSH Hijacking', + reference: 'https://attack.mitre.org/techniques/T1184', + tactics: 'lateral-movement', + value: 'sshHijacking', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.scheduledTaskDescription', + { defaultMessage: 'Scheduled Task (T1053)' } + ), + id: 'T1053', + name: 'Scheduled Task', + reference: 'https://attack.mitre.org/techniques/T1053', + tactics: 'execution,persistence,privilege-escalation', + value: 'scheduledTask', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.scheduledTransferDescription', + { defaultMessage: 'Scheduled Transfer (T1029)' } + ), + id: 'T1029', + name: 'Scheduled Transfer', + reference: 'https://attack.mitre.org/techniques/T1029', + tactics: 'exfiltration', + value: 'scheduledTransfer', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.screenCaptureDescription', + { defaultMessage: 'Screen Capture (T1113)' } + ), + id: 'T1113', + name: 'Screen Capture', + reference: 'https://attack.mitre.org/techniques/T1113', + tactics: 'collection', + value: 'screenCapture', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.screensaverDescription', + { defaultMessage: 'Screensaver (T1180)' } + ), + id: 'T1180', + name: 'Screensaver', + reference: 'https://attack.mitre.org/techniques/T1180', + tactics: 'persistence', + value: 'screensaver', + }, + { + label: i18n.translate('xpack.siem.detectionEngine.mitreAttackTechniques.scriptingDescription', { + defaultMessage: 'Scripting (T1064)', + }), + id: 'T1064', + name: 'Scripting', + reference: 'https://attack.mitre.org/techniques/T1064', + tactics: 'defense-evasion,execution', + value: 'scripting', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.securitySoftwareDiscoveryDescription', + { defaultMessage: 'Security Software Discovery (T1063)' } + ), + id: 'T1063', + name: 'Security Software Discovery', + reference: 'https://attack.mitre.org/techniques/T1063', + tactics: 'discovery', + value: 'securitySoftwareDiscovery', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.securitySupportProviderDescription', + { defaultMessage: 'Security Support Provider (T1101)' } + ), + id: 'T1101', + name: 'Security Support Provider', + reference: 'https://attack.mitre.org/techniques/T1101', + tactics: 'persistence', + value: 'securitySupportProvider', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.securitydMemoryDescription', + { defaultMessage: 'Securityd Memory (T1167)' } + ), + id: 'T1167', + name: 'Securityd Memory', + reference: 'https://attack.mitre.org/techniques/T1167', + tactics: 'credential-access', + value: 'securitydMemory', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.serverSoftwareComponentDescription', + { defaultMessage: 'Server Software Component (T1505)' } + ), + id: 'T1505', + name: 'Server Software Component', + reference: 'https://attack.mitre.org/techniques/T1505', + tactics: 'persistence', + value: 'serverSoftwareComponent', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.serviceExecutionDescription', + { defaultMessage: 'Service Execution (T1035)' } + ), + id: 'T1035', + name: 'Service Execution', + reference: 'https://attack.mitre.org/techniques/T1035', + tactics: 'execution', + value: 'serviceExecution', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.serviceRegistryPermissionsWeaknessDescription', + { defaultMessage: 'Service Registry Permissions Weakness (T1058)' } + ), + id: 'T1058', + name: 'Service Registry Permissions Weakness', + reference: 'https://attack.mitre.org/techniques/T1058', + tactics: 'persistence,privilege-escalation', + value: 'serviceRegistryPermissionsWeakness', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.serviceStopDescription', + { defaultMessage: 'Service Stop (T1489)' } + ), + id: 'T1489', + name: 'Service Stop', + reference: 'https://attack.mitre.org/techniques/T1489', + tactics: 'impact', + value: 'serviceStop', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.setuidAndSetgidDescription', + { defaultMessage: 'Setuid and Setgid (T1166)' } + ), + id: 'T1166', + name: 'Setuid and Setgid', + reference: 'https://attack.mitre.org/techniques/T1166', + tactics: 'privilege-escalation,persistence', + value: 'setuidAndSetgid', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.sharedWebrootDescription', + { defaultMessage: 'Shared Webroot (T1051)' } + ), + id: 'T1051', + name: 'Shared Webroot', + reference: 'https://attack.mitre.org/techniques/T1051', + tactics: 'lateral-movement', + value: 'sharedWebroot', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.shortcutModificationDescription', + { defaultMessage: 'Shortcut Modification (T1023)' } + ), + id: 'T1023', + name: 'Shortcut Modification', + reference: 'https://attack.mitre.org/techniques/T1023', + tactics: 'persistence', + value: 'shortcutModification', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.signedBinaryProxyExecutionDescription', + { defaultMessage: 'Signed Binary Proxy Execution (T1218)' } + ), + id: 'T1218', + name: 'Signed Binary Proxy Execution', + reference: 'https://attack.mitre.org/techniques/T1218', + tactics: 'defense-evasion,execution', + value: 'signedBinaryProxyExecution', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.signedScriptProxyExecutionDescription', + { defaultMessage: 'Signed Script Proxy Execution (T1216)' } + ), + id: 'T1216', + name: 'Signed Script Proxy Execution', + reference: 'https://attack.mitre.org/techniques/T1216', + tactics: 'defense-evasion,execution', + value: 'signedScriptProxyExecution', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.softwareDiscoveryDescription', + { defaultMessage: 'Software Discovery (T1518)' } + ), + id: 'T1518', + name: 'Software Discovery', + reference: 'https://attack.mitre.org/techniques/T1518', + tactics: 'discovery', + value: 'softwareDiscovery', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.softwarePackingDescription', + { defaultMessage: 'Software Packing (T1045)' } + ), + id: 'T1045', + name: 'Software Packing', + reference: 'https://attack.mitre.org/techniques/T1045', + tactics: 'defense-evasion', + value: 'softwarePacking', + }, + { + label: i18n.translate('xpack.siem.detectionEngine.mitreAttackTechniques.sourceDescription', { + defaultMessage: 'Source (T1153)', + }), + id: 'T1153', + name: 'Source', + reference: 'https://attack.mitre.org/techniques/T1153', + tactics: 'execution', + value: 'source', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.spaceAfterFilenameDescription', + { defaultMessage: 'Space after Filename (T1151)' } + ), + id: 'T1151', + name: 'Space after Filename', + reference: 'https://attack.mitre.org/techniques/T1151', + tactics: 'defense-evasion,execution', + value: 'spaceAfterFilename', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.spearphishingAttachmentDescription', + { defaultMessage: 'Spearphishing Attachment (T1193)' } + ), + id: 'T1193', + name: 'Spearphishing Attachment', + reference: 'https://attack.mitre.org/techniques/T1193', + tactics: 'initial-access', + value: 'spearphishingAttachment', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.spearphishingLinkDescription', + { defaultMessage: 'Spearphishing Link (T1192)' } + ), + id: 'T1192', + name: 'Spearphishing Link', + reference: 'https://attack.mitre.org/techniques/T1192', + tactics: 'initial-access', + value: 'spearphishingLink', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.spearphishingViaServiceDescription', + { defaultMessage: 'Spearphishing via Service (T1194)' } + ), + id: 'T1194', + name: 'Spearphishing via Service', + reference: 'https://attack.mitre.org/techniques/T1194', + tactics: 'initial-access', + value: 'spearphishingViaService', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.standardApplicationLayerProtocolDescription', + { defaultMessage: 'Standard Application Layer Protocol (T1071)' } + ), + id: 'T1071', + name: 'Standard Application Layer Protocol', + reference: 'https://attack.mitre.org/techniques/T1071', + tactics: 'command-and-control', + value: 'standardApplicationLayerProtocol', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.standardCryptographicProtocolDescription', + { defaultMessage: 'Standard Cryptographic Protocol (T1032)' } + ), + id: 'T1032', + name: 'Standard Cryptographic Protocol', + reference: 'https://attack.mitre.org/techniques/T1032', + tactics: 'command-and-control', + value: 'standardCryptographicProtocol', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.standardNonApplicationLayerProtocolDescription', + { defaultMessage: 'Standard Non-Application Layer Protocol (T1095)' } + ), + id: 'T1095', + name: 'Standard Non-Application Layer Protocol', + reference: 'https://attack.mitre.org/techniques/T1095', + tactics: 'command-and-control', + value: 'standardNonApplicationLayerProtocol', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.startupItemsDescription', + { defaultMessage: 'Startup Items (T1165)' } + ), + id: 'T1165', + name: 'Startup Items', + reference: 'https://attack.mitre.org/techniques/T1165', + tactics: 'persistence,privilege-escalation', + value: 'startupItems', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.stealApplicationAccessTokenDescription', + { defaultMessage: 'Steal Application Access Token (T1528)' } + ), + id: 'T1528', + name: 'Steal Application Access Token', + reference: 'https://attack.mitre.org/techniques/T1528', + tactics: 'credential-access', + value: 'stealApplicationAccessToken', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.stealWebSessionCookieDescription', + { defaultMessage: 'Steal Web Session Cookie (T1539)' } + ), + id: 'T1539', + name: 'Steal Web Session Cookie', + reference: 'https://attack.mitre.org/techniques/T1539', + tactics: 'credential-access', + value: 'stealWebSessionCookie', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.storedDataManipulationDescription', + { defaultMessage: 'Stored Data Manipulation (T1492)' } + ), + id: 'T1492', + name: 'Stored Data Manipulation', + reference: 'https://attack.mitre.org/techniques/T1492', + tactics: 'impact', + value: 'storedDataManipulation', + }, + { + label: i18n.translate('xpack.siem.detectionEngine.mitreAttackTechniques.sudoDescription', { + defaultMessage: 'Sudo (T1169)', + }), + id: 'T1169', + name: 'Sudo', + reference: 'https://attack.mitre.org/techniques/T1169', + tactics: 'privilege-escalation', + value: 'sudo', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.sudoCachingDescription', + { defaultMessage: 'Sudo Caching (T1206)' } + ), + id: 'T1206', + name: 'Sudo Caching', + reference: 'https://attack.mitre.org/techniques/T1206', + tactics: 'privilege-escalation', + value: 'sudoCaching', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.supplyChainCompromiseDescription', + { defaultMessage: 'Supply Chain Compromise (T1195)' } + ), + id: 'T1195', + name: 'Supply Chain Compromise', + reference: 'https://attack.mitre.org/techniques/T1195', + tactics: 'initial-access', + value: 'supplyChainCompromise', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.systemFirmwareDescription', + { defaultMessage: 'System Firmware (T1019)' } + ), + id: 'T1019', + name: 'System Firmware', + reference: 'https://attack.mitre.org/techniques/T1019', + tactics: 'persistence', + value: 'systemFirmware', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.systemInformationDiscoveryDescription', + { defaultMessage: 'System Information Discovery (T1082)' } + ), + id: 'T1082', + name: 'System Information Discovery', + reference: 'https://attack.mitre.org/techniques/T1082', + tactics: 'discovery', + value: 'systemInformationDiscovery', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.systemNetworkConfigurationDiscoveryDescription', + { defaultMessage: 'System Network Configuration Discovery (T1016)' } + ), + id: 'T1016', + name: 'System Network Configuration Discovery', + reference: 'https://attack.mitre.org/techniques/T1016', + tactics: 'discovery', + value: 'systemNetworkConfigurationDiscovery', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.systemNetworkConnectionsDiscoveryDescription', + { defaultMessage: 'System Network Connections Discovery (T1049)' } + ), + id: 'T1049', + name: 'System Network Connections Discovery', + reference: 'https://attack.mitre.org/techniques/T1049', + tactics: 'discovery', + value: 'systemNetworkConnectionsDiscovery', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.systemOwnerUserDiscoveryDescription', + { defaultMessage: 'System Owner/User Discovery (T1033)' } + ), + id: 'T1033', + name: 'System Owner/User Discovery', + reference: 'https://attack.mitre.org/techniques/T1033', + tactics: 'discovery', + value: 'systemOwnerUserDiscovery', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.systemServiceDiscoveryDescription', + { defaultMessage: 'System Service Discovery (T1007)' } + ), + id: 'T1007', + name: 'System Service Discovery', + reference: 'https://attack.mitre.org/techniques/T1007', + tactics: 'discovery', + value: 'systemServiceDiscovery', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.systemShutdownRebootDescription', + { defaultMessage: 'System Shutdown/Reboot (T1529)' } + ), + id: 'T1529', + name: 'System Shutdown/Reboot', + reference: 'https://attack.mitre.org/techniques/T1529', + tactics: 'impact', + value: 'systemShutdownReboot', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.systemTimeDiscoveryDescription', + { defaultMessage: 'System Time Discovery (T1124)' } + ), + id: 'T1124', + name: 'System Time Discovery', + reference: 'https://attack.mitre.org/techniques/T1124', + tactics: 'discovery', + value: 'systemTimeDiscovery', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.systemdServiceDescription', + { defaultMessage: 'Systemd Service (T1501)' } + ), + id: 'T1501', + name: 'Systemd Service', + reference: 'https://attack.mitre.org/techniques/T1501', + tactics: 'persistence', + value: 'systemdService', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.taintSharedContentDescription', + { defaultMessage: 'Taint Shared Content (T1080)' } + ), + id: 'T1080', + name: 'Taint Shared Content', + reference: 'https://attack.mitre.org/techniques/T1080', + tactics: 'lateral-movement', + value: 'taintSharedContent', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.templateInjectionDescription', + { defaultMessage: 'Template Injection (T1221)' } + ), + id: 'T1221', + name: 'Template Injection', + reference: 'https://attack.mitre.org/techniques/T1221', + tactics: 'defense-evasion', + value: 'templateInjection', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.thirdPartySoftwareDescription', + { defaultMessage: 'Third-party Software (T1072)' } + ), + id: 'T1072', + name: 'Third-party Software', + reference: 'https://attack.mitre.org/techniques/T1072', + tactics: 'execution,lateral-movement', + value: 'thirdPartySoftware', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.timeProvidersDescription', + { defaultMessage: 'Time Providers (T1209)' } + ), + id: 'T1209', + name: 'Time Providers', + reference: 'https://attack.mitre.org/techniques/T1209', + tactics: 'persistence', + value: 'timeProviders', + }, + { + label: i18n.translate('xpack.siem.detectionEngine.mitreAttackTechniques.timestompDescription', { + defaultMessage: 'Timestomp (T1099)', + }), + id: 'T1099', + name: 'Timestomp', + reference: 'https://attack.mitre.org/techniques/T1099', + tactics: 'defense-evasion', + value: 'timestomp', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.transferDataToCloudAccountDescription', + { defaultMessage: 'Transfer Data to Cloud Account (T1537)' } + ), + id: 'T1537', + name: 'Transfer Data to Cloud Account', + reference: 'https://attack.mitre.org/techniques/T1537', + tactics: 'exfiltration', + value: 'transferDataToCloudAccount', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.transmittedDataManipulationDescription', + { defaultMessage: 'Transmitted Data Manipulation (T1493)' } + ), + id: 'T1493', + name: 'Transmitted Data Manipulation', + reference: 'https://attack.mitre.org/techniques/T1493', + tactics: 'impact', + value: 'transmittedDataManipulation', + }, + { + label: i18n.translate('xpack.siem.detectionEngine.mitreAttackTechniques.trapDescription', { + defaultMessage: 'Trap (T1154)', + }), + id: 'T1154', + name: 'Trap', + reference: 'https://attack.mitre.org/techniques/T1154', + tactics: 'execution,persistence', + value: 'trap', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.trustedDeveloperUtilitiesDescription', + { defaultMessage: 'Trusted Developer Utilities (T1127)' } + ), + id: 'T1127', + name: 'Trusted Developer Utilities', + reference: 'https://attack.mitre.org/techniques/T1127', + tactics: 'defense-evasion,execution', + value: 'trustedDeveloperUtilities', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.trustedRelationshipDescription', + { defaultMessage: 'Trusted Relationship (T1199)' } + ), + id: 'T1199', + name: 'Trusted Relationship', + reference: 'https://attack.mitre.org/techniques/T1199', + tactics: 'initial-access', + value: 'trustedRelationship', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.twoFactorAuthenticationInterceptionDescription', + { defaultMessage: 'Two-Factor Authentication Interception (T1111)' } + ), + id: 'T1111', + name: 'Two-Factor Authentication Interception', + reference: 'https://attack.mitre.org/techniques/T1111', + tactics: 'credential-access', + value: 'twoFactorAuthenticationInterception', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.uncommonlyUsedPortDescription', + { defaultMessage: 'Uncommonly Used Port (T1065)' } + ), + id: 'T1065', + name: 'Uncommonly Used Port', + reference: 'https://attack.mitre.org/techniques/T1065', + tactics: 'command-and-control', + value: 'uncommonlyUsedPort', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.unusedUnsupportedCloudRegionsDescription', + { defaultMessage: 'Unused/Unsupported Cloud Regions (T1535)' } + ), + id: 'T1535', + name: 'Unused/Unsupported Cloud Regions', + reference: 'https://attack.mitre.org/techniques/T1535', + tactics: 'defense-evasion', + value: 'unusedUnsupportedCloudRegions', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.userExecutionDescription', + { defaultMessage: 'User Execution (T1204)' } + ), + id: 'T1204', + name: 'User Execution', + reference: 'https://attack.mitre.org/techniques/T1204', + tactics: 'execution', + value: 'userExecution', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.validAccountsDescription', + { defaultMessage: 'Valid Accounts (T1078)' } + ), + id: 'T1078', + name: 'Valid Accounts', + reference: 'https://attack.mitre.org/techniques/T1078', + tactics: 'defense-evasion,persistence,privilege-escalation,initial-access', + value: 'validAccounts', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.videoCaptureDescription', + { defaultMessage: 'Video Capture (T1125)' } + ), + id: 'T1125', + name: 'Video Capture', + reference: 'https://attack.mitre.org/techniques/T1125', + tactics: 'collection', + value: 'videoCapture', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.virtualizationSandboxEvasionDescription', + { defaultMessage: 'Virtualization/Sandbox Evasion (T1497)' } + ), + id: 'T1497', + name: 'Virtualization/Sandbox Evasion', + reference: 'https://attack.mitre.org/techniques/T1497', + tactics: 'defense-evasion,discovery', + value: 'virtualizationSandboxEvasion', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.webServiceDescription', + { defaultMessage: 'Web Service (T1102)' } + ), + id: 'T1102', + name: 'Web Service', + reference: 'https://attack.mitre.org/techniques/T1102', + tactics: 'command-and-control,defense-evasion', + value: 'webService', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.webSessionCookieDescription', + { defaultMessage: 'Web Session Cookie (T1506)' } + ), + id: 'T1506', + name: 'Web Session Cookie', + reference: 'https://attack.mitre.org/techniques/T1506', + tactics: 'defense-evasion,lateral-movement', + value: 'webSessionCookie', + }, + { + label: i18n.translate('xpack.siem.detectionEngine.mitreAttackTechniques.webShellDescription', { + defaultMessage: 'Web Shell (T1100)', + }), + id: 'T1100', + name: 'Web Shell', + reference: 'https://attack.mitre.org/techniques/T1100', + tactics: 'persistence,privilege-escalation', + value: 'webShell', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.windowsAdminSharesDescription', + { defaultMessage: 'Windows Admin Shares (T1077)' } + ), + id: 'T1077', + name: 'Windows Admin Shares', + reference: 'https://attack.mitre.org/techniques/T1077', + tactics: 'lateral-movement', + value: 'windowsAdminShares', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.windowsManagementInstrumentationDescription', + { defaultMessage: 'Windows Management Instrumentation (T1047)' } + ), + id: 'T1047', + name: 'Windows Management Instrumentation', + reference: 'https://attack.mitre.org/techniques/T1047', + tactics: 'execution', + value: 'windowsManagementInstrumentation', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.windowsManagementInstrumentationEventSubscriptionDescription', + { defaultMessage: 'Windows Management Instrumentation Event Subscription (T1084)' } + ), + id: 'T1084', + name: 'Windows Management Instrumentation Event Subscription', + reference: 'https://attack.mitre.org/techniques/T1084', + tactics: 'persistence', + value: 'windowsManagementInstrumentationEventSubscription', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.windowsRemoteManagementDescription', + { defaultMessage: 'Windows Remote Management (T1028)' } + ), + id: 'T1028', + name: 'Windows Remote Management', + reference: 'https://attack.mitre.org/techniques/T1028', + tactics: 'execution,lateral-movement', + value: 'windowsRemoteManagement', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.winlogonHelperDllDescription', + { defaultMessage: 'Winlogon Helper DLL (T1004)' } + ), + id: 'T1004', + name: 'Winlogon Helper DLL', + reference: 'https://attack.mitre.org/techniques/T1004', + tactics: 'persistence', + value: 'winlogonHelperDll', + }, + { + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.xslScriptProcessingDescription', + { defaultMessage: 'XSL Script Processing (T1220)' } + ), + id: 'T1220', + name: 'XSL Script Processing', + reference: 'https://attack.mitre.org/techniques/T1220', + tactics: 'defense-evasion,execution', + value: 'xslScriptProcessing', + }, +]; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/mitre/types.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/mitre/types.ts new file mode 100644 index 0000000000000..a1e7a2e66ab83 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/mitre/types.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface MitreOptions { + id: string; + name: string; + reference: string; + value: string; +} + +export interface MitreTacticsOptions extends MitreOptions { + text: string; +} + +export interface MitreTechniquesOptions extends MitreOptions { + label: string; + tactics: string; +} diff --git a/x-pack/legacy/plugins/siem/scripts/extract_tactics_techniques_mitre.js b/x-pack/legacy/plugins/siem/scripts/extract_tactics_techniques_mitre.js new file mode 100644 index 0000000000000..9648acecfe461 --- /dev/null +++ b/x-pack/legacy/plugins/siem/scripts/extract_tactics_techniques_mitre.js @@ -0,0 +1,113 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +require('../../../../../src/setup_node_env'); + +const fs = require('fs'); +// eslint-disable-next-line import/no-extraneous-dependencies +const fetch = require('node-fetch'); +const { camelCase } = require('lodash'); +const { resolve } = require('path'); + + +const OUTPUT_DIRECTORY = resolve('public', 'pages', 'detection_engine', 'mitre'); +const MITRE_ENTREPRISE_ATTACK_URL = 'https://raw.githubusercontent.com/mitre/cti/master/enterprise-attack/enterprise-attack.json'; + + + +const getTacticsOptions = tactics => tactics.map(t => `{ + id: '${t.id}', + name: '${t.name}', + reference: '${t.reference}', + text: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTactics.${camelCase(t.name)}Description', { + defaultMessage: '${t.name} (${t.id})' + }), + value: '${camelCase(t.name)}' +}`.replace(/(\r\n|\n|\r)/gm, ' ')); + +const getTechniquesOptions = techniques => techniques.map(t => `{ + label: i18n.translate( + 'xpack.siem.detectionEngine.mitreAttackTechniques.${camelCase(t.name)}Description', { + defaultMessage: '${t.name} (${t.id})' + }), + id: '${t.id}', + name: '${t.name}', + reference: '${t.reference}', + tactics: '${t.tactics.join()}', + value: '${camelCase(t.name)}' +}`.replace(/(\r\n|\n|\r)/gm, ' ')); + +const getIdReference = references => references.reduce((obj, extRef) => { + if (extRef.source_name === 'mitre-attack') { + return { + id: extRef.external_id, reference: extRef.url + }; + } + return obj; +}, { id: '', reference: '' }); + +async function main() { + fetch(MITRE_ENTREPRISE_ATTACK_URL) + .then(res => res.json()) + .then(json => { + const mitreData = json.objects; + const tactics = mitreData.filter(obj => obj.type === 'x-mitre-tactic').reduce((acc, item) => { + const { id, reference } = getIdReference(item.external_references); + + return [...acc, { + name: item.name, + id, + reference, + }]; + }, []); + const techniques = mitreData.filter(obj => obj.type === 'attack-pattern').reduce((acc, item) => { + let tactics = []; + const { id, reference } = getIdReference(item.external_references); + if (item.kill_chain_phases != null && item.kill_chain_phases.length > 0) { + item.kill_chain_phases.forEach(tactic => { + tactics = [...tactics, tactic.phase_name]; + }); + } + + return [...acc, { + name: item.name, + id, + reference, + tactics, + }]; + }, []); + + const body = + `/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + + import { i18n } from '@kbn/i18n'; + + import { MitreTacticsOptions, MitreTechniquesOptions } from './types'; + + export const tactics = ${JSON.stringify(tactics, null, 2)}; + + export const tacticsOptions: MitreTacticsOptions[] = + ${JSON.stringify(getTacticsOptions(tactics), null, 2).replace(/}"/g, '}').replace(/"{/g, '{')}; + + export const techniques = ${JSON.stringify(techniques, null, 2)}; + + export const techniquesOptions: MitreTechniquesOptions[] = + ${JSON.stringify(getTechniquesOptions(techniques), null, 2).replace(/}"/g, '}').replace(/"{/g, '{')}; + `; + + fs.writeFileSync(`${OUTPUT_DIRECTORY}/mitre_tactics_techniques.ts`, body, 'utf-8'); + + }); +} + +if (require.main === module) { + main(); +} From e71deb26832d65a9f488f7c2ab1ce73712b5e629 Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Tue, 10 Dec 2019 09:24:13 -0700 Subject: [PATCH 04/40] [Reporting/Screenshots] Do not fail the report if request is aborted (#52344) * [Reporting/Screenshots] Do not fail the report if request is aborted * take pageRequestFailed out of pageExit observable --- .../browsers/chromium/driver_factory/index.ts | 43 ++++++++----------- 1 file changed, 19 insertions(+), 24 deletions(-) diff --git a/x-pack/legacy/plugins/reporting/server/browsers/chromium/driver_factory/index.ts b/x-pack/legacy/plugins/reporting/server/browsers/chromium/driver_factory/index.ts index ca26f7d41c12a..daa7df343f8aa 100644 --- a/x-pack/legacy/plugins/reporting/server/browsers/chromium/driver_factory/index.ts +++ b/x-pack/legacy/plugins/reporting/server/browsers/chromium/driver_factory/index.ts @@ -15,7 +15,7 @@ import { } from 'puppeteer'; import del from 'del'; import * as Rx from 'rxjs'; -import { ignoreElements, mergeMap, tap } from 'rxjs/operators'; +import { ignoreElements, map, mergeMap, tap } from 'rxjs/operators'; import { InnerSubscriber } from 'rxjs/internal/InnerSubscriber'; import { BrowserConfig, NetworkPolicy } from '../../../../types'; @@ -144,7 +144,7 @@ export class HeadlessChromiumDriverFactory { terminate$ .pipe( tap(signal => { - this.logger.debug(`Observer got signal: ${signal}`); + this.logger.debug(`Termination signal received: ${signal}`); }), ignoreElements() ) @@ -156,7 +156,6 @@ export class HeadlessChromiumDriverFactory { this.getProcessLogger(browser).subscribe(); const driver$ = Rx.of(new HeadlessChromiumDriver(page, { inspect: this.browserConfig.inspect, networkPolicy: this.networkPolicy })); // prettier-ignore - const exit$ = this.getPageExit(browser, page); observer.next({ driver$, exit$ }); @@ -173,9 +172,9 @@ export class HeadlessChromiumDriverFactory { }); } - getBrowserLogger(page: Page): Rx.Observable { - return Rx.fromEvent(page, 'console').pipe( - tap(line => { + getBrowserLogger(page: Page): Rx.Observable { + const consoleMessages$ = Rx.fromEvent(page, 'console').pipe( + map(line => { if (line.type() === 'error') { this.logger.error(line.text(), ['headless-browser-console']); } else { @@ -183,6 +182,19 @@ export class HeadlessChromiumDriverFactory { } }) ); + + const pageRequestFailed$ = Rx.fromEvent(page, 'requestfailed').pipe( + map(req => { + const failure = req.failure && req.failure(); + if (failure) { + this.logger.warning( + `Request to [${req.url()}] failed! [${failure.errorText}]. This error will be ignored.` + ); + } + }) + ); + + return Rx.merge(consoleMessages$, pageRequestFailed$); } getProcessLogger(browser: Browser) { @@ -208,18 +220,6 @@ export class HeadlessChromiumDriverFactory { mergeMap(err => Rx.throwError(err)) ); - const pageRequestFailed$ = Rx.fromEvent(page, 'requestfailed').pipe( - mergeMap(req => { - const failure = req.failure && req.failure(); - if (failure) { - return Rx.throwError( - new Error(`Request to [${req.url()}] failed! [${failure.errorText}]`) - ); - } - return Rx.throwError(new Error(`Unknown failure!`)); - }) - ); - const browserDisconnect$ = Rx.fromEvent(browser, 'disconnected').pipe( mergeMap(() => Rx.throwError( @@ -230,11 +230,6 @@ export class HeadlessChromiumDriverFactory { ) ); - return Rx.merge( - pageError$, - uncaughtExceptionPageError$, - pageRequestFailed$, - browserDisconnect$ - ); + return Rx.merge(pageError$, uncaughtExceptionPageError$, browserDisconnect$); } } From 618e70433b8455322be08b9175dfcf66ba5d80be Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Tue, 10 Dec 2019 17:32:56 +0100 Subject: [PATCH 05/40] Shim oss telemetry (#51168) --- .../legacy/plugins/oss_telemetry/index.d.ts | 61 ------------------- x-pack/legacy/plugins/oss_telemetry/index.js | 24 -------- x-pack/legacy/plugins/oss_telemetry/index.ts | 41 +++++++++++++ .../server/lib/collectors/index.ts | 7 +-- .../get_usage_collector.test.ts | 46 +++++--------- .../visualizations/get_usage_collector.ts | 16 +++-- .../register_usage_collector.ts | 10 +-- .../oss_telemetry/server/lib/tasks/index.ts | 47 ++++++++++---- .../tasks/visualizations/task_runner.test.ts | 23 ++++--- .../lib/tasks/visualizations/task_runner.ts | 48 ++++++++------- .../plugins/oss_telemetry/server/plugin.ts | 50 +++++++++++++++ .../plugins/oss_telemetry/test_utils/index.ts | 51 ++++++++-------- 12 files changed, 223 insertions(+), 201 deletions(-) delete mode 100644 x-pack/legacy/plugins/oss_telemetry/index.d.ts delete mode 100644 x-pack/legacy/plugins/oss_telemetry/index.js create mode 100644 x-pack/legacy/plugins/oss_telemetry/index.ts create mode 100644 x-pack/legacy/plugins/oss_telemetry/server/plugin.ts diff --git a/x-pack/legacy/plugins/oss_telemetry/index.d.ts b/x-pack/legacy/plugins/oss_telemetry/index.d.ts deleted file mode 100644 index 1b592dabf2053..0000000000000 --- a/x-pack/legacy/plugins/oss_telemetry/index.d.ts +++ /dev/null @@ -1,61 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -export interface VisState { - type: string; -} - -export interface Visualization { - visState: string; -} - -export interface SavedObjectDoc { - _id: string; - _source: { - visualization: Visualization; - type: string; - }; -} - -export interface ESQueryResponse { - hits: { - hits: SavedObjectDoc[]; - }; -} - -export interface TaskInstance { - state: { - runs: number; - stats: any; - }; - error?: any; -} - -export interface HapiServer { - plugins: { - xpack_main: any; - elasticsearch: { - getCluster: ( - cluster: string - ) => { - callWithInternalUser: () => Promise; - }; - }; - task_manager: { - registerTaskDefinitions: (opts: any) => void; - ensureScheduled: (opts: any) => Promise; - fetch: ( - opts: any - ) => Promise<{ - docs: TaskInstance[]; - }>; - }; - }; - config: () => { - get: (prop: string) => any; - }; - log: (context: string[], message: string) => void; -} diff --git a/x-pack/legacy/plugins/oss_telemetry/index.js b/x-pack/legacy/plugins/oss_telemetry/index.js deleted file mode 100644 index f86baef020aa2..0000000000000 --- a/x-pack/legacy/plugins/oss_telemetry/index.js +++ /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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { registerCollectors } from './server/lib/collectors'; -import { registerTasks, scheduleTasks } from './server/lib/tasks'; -import { PLUGIN_ID } from './constants'; - -export const ossTelemetry = (kibana) => { - return new kibana.Plugin({ - id: PLUGIN_ID, - require: ['elasticsearch', 'xpack_main'], - configPrefix: 'xpack.oss_telemetry', - - init(server) { - const { usageCollection } = server.newPlatform.setup.plugins; - registerCollectors(usageCollection, server); - registerTasks(server); - scheduleTasks(server); - } - }); -}; diff --git a/x-pack/legacy/plugins/oss_telemetry/index.ts b/x-pack/legacy/plugins/oss_telemetry/index.ts new file mode 100644 index 0000000000000..8b16c7cf13cad --- /dev/null +++ b/x-pack/legacy/plugins/oss_telemetry/index.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Logger, PluginInitializerContext } from 'kibana/server'; +import { PLUGIN_ID } from './constants'; +import { OssTelemetryPlugin } from './server/plugin'; +import { LegacyPluginInitializer } from '../../../../src/legacy/plugin_discovery/types'; + +export const ossTelemetry: LegacyPluginInitializer = kibana => { + return new kibana.Plugin({ + id: PLUGIN_ID, + require: ['elasticsearch', 'xpack_main'], + configPrefix: 'xpack.oss_telemetry', + + init(server) { + const plugin = new OssTelemetryPlugin({ + logger: { + get: () => + ({ + info: (message: string) => server.log(['info', 'task_manager'], message), + debug: (message: string) => server.log(['debug', 'task_manager'], message), + warn: (message: string) => server.log(['warn', 'task_manager'], message), + error: (message: string) => server.log(['error', 'task_manager'], message), + } as Logger), + }, + } as PluginInitializerContext); + plugin.setup(server.newPlatform.setup.core, { + usageCollection: server.newPlatform.setup.plugins.usageCollection, + taskManager: server.plugins.task_manager, + __LEGACY: { + config: server.config(), + xpackMainStatus: ((server.plugins.xpack_main as unknown) as { status: any }).status + .plugin, + }, + }); + }, + }); +}; diff --git a/x-pack/legacy/plugins/oss_telemetry/server/lib/collectors/index.ts b/x-pack/legacy/plugins/oss_telemetry/server/lib/collectors/index.ts index 0121ed4304d26..3b47099fdc462 100644 --- a/x-pack/legacy/plugins/oss_telemetry/server/lib/collectors/index.ts +++ b/x-pack/legacy/plugins/oss_telemetry/server/lib/collectors/index.ts @@ -4,10 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; -import { HapiServer } from '../../../'; import { registerVisualizationsCollector } from './visualizations/register_usage_collector'; +import { OssTelemetrySetupDependencies } from '../../plugin'; -export function registerCollectors(usageCollection: UsageCollectionSetup, server: HapiServer) { - registerVisualizationsCollector(usageCollection, server); +export function registerCollectors(deps: OssTelemetrySetupDependencies) { + registerVisualizationsCollector(deps.usageCollection, deps.taskManager); } diff --git a/x-pack/legacy/plugins/oss_telemetry/server/lib/collectors/visualizations/get_usage_collector.test.ts b/x-pack/legacy/plugins/oss_telemetry/server/lib/collectors/visualizations/get_usage_collector.test.ts index d316562c826d6..ec35266646650 100644 --- a/x-pack/legacy/plugins/oss_telemetry/server/lib/collectors/visualizations/get_usage_collector.test.ts +++ b/x-pack/legacy/plugins/oss_telemetry/server/lib/collectors/visualizations/get_usage_collector.test.ts @@ -4,24 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import sinon from 'sinon'; -import { HapiServer } from '../../../../'; -import { - getMockCallWithInternal, - getMockKbnServer, - getMockTaskFetch, -} from '../../../../test_utils'; +import { getMockTaskFetch, getMockTaskManager } from '../../../../test_utils'; import { getUsageCollector } from './get_usage_collector'; describe('getVisualizationsCollector#fetch', () => { - let mockKbnServer: HapiServer; - - beforeEach(() => { - mockKbnServer = getMockKbnServer(getMockCallWithInternal(), getMockTaskFetch()); - }); - test('can return empty stats', async () => { - const { type, fetch } = getUsageCollector(mockKbnServer); + const { type, fetch } = getUsageCollector(getMockTaskManager()); expect(type).toBe('visualization_types'); const fetchResult = await fetch(); expect(fetchResult).toEqual({}); @@ -34,11 +22,11 @@ describe('getVisualizationsCollector#fetch', () => { runs: 1, stats: { comic_books: { total: 16, max: 12, min: 2, avg: 6 } }, }, + taskType: 'test', + params: {}, }, ]); - mockKbnServer = getMockKbnServer(getMockCallWithInternal(), mockTaskFetch); - - const { type, fetch } = getUsageCollector(mockKbnServer); + const { type, fetch } = getUsageCollector(getMockTaskManager(mockTaskFetch)); expect(type).toBe('visualization_types'); const fetchResult = await fetch(); expect(fetchResult).toEqual({ comic_books: { avg: 6, max: 12, min: 2, total: 16 } }); @@ -46,23 +34,21 @@ describe('getVisualizationsCollector#fetch', () => { describe('Error handling', () => { test('Silently handles Task Manager NotInitialized', async () => { - const mockTaskFetch = sinon.stub(); - mockTaskFetch.rejects( - new Error('NotInitialized taskManager is still waiting for plugins to load') - ); - mockKbnServer = getMockKbnServer(getMockCallWithInternal(), mockTaskFetch); - - const { fetch } = getUsageCollector(mockKbnServer); - await expect(fetch()).resolves.toBe(undefined); + const mockTaskFetch = jest.fn(() => { + throw new Error('NotInitialized taskManager is still waiting for plugins to load'); + }); + const { fetch } = getUsageCollector(getMockTaskManager(mockTaskFetch)); + const result = await fetch(); + expect(result).toBe(undefined); }); // In real life, the CollectorSet calls fetch and handles errors test('defers the errors', async () => { - const mockTaskFetch = sinon.stub(); - mockTaskFetch.rejects(new Error('BOOM')); - mockKbnServer = getMockKbnServer(getMockCallWithInternal(), mockTaskFetch); + const mockTaskFetch = jest.fn(() => { + throw new Error('BOOM'); + }); - const { fetch } = getUsageCollector(mockKbnServer); - await expect(fetch()).rejects.toMatchObject(new Error('BOOM')); + const { fetch } = getUsageCollector(getMockTaskManager(mockTaskFetch)); + await expect(fetch()).rejects.toThrowErrorMatchingInlineSnapshot(`"BOOM"`); }); }); }); diff --git a/x-pack/legacy/plugins/oss_telemetry/server/lib/collectors/visualizations/get_usage_collector.ts b/x-pack/legacy/plugins/oss_telemetry/server/lib/collectors/visualizations/get_usage_collector.ts index 63640c87f80a6..680cb97e0fda3 100644 --- a/x-pack/legacy/plugins/oss_telemetry/server/lib/collectors/visualizations/get_usage_collector.ts +++ b/x-pack/legacy/plugins/oss_telemetry/server/lib/collectors/visualizations/get_usage_collector.ts @@ -5,17 +5,15 @@ */ import { get } from 'lodash'; -import { HapiServer } from '../../../../'; +import { PluginSetupContract as TaskManagerPluginSetupContract } from '../../../../../task_manager/plugin'; import { PLUGIN_ID, VIS_TELEMETRY_TASK, VIS_USAGE_TYPE } from '../../../../constants'; -async function isTaskManagerReady(server: HapiServer) { - const result = await fetch(server); +async function isTaskManagerReady(taskManager: TaskManagerPluginSetupContract | undefined) { + const result = await fetch(taskManager); return result !== null; } -async function fetch(server: HapiServer) { - const taskManager = server.plugins.task_manager; - +async function fetch(taskManager: TaskManagerPluginSetupContract | undefined) { if (!taskManager) { return null; } @@ -40,12 +38,12 @@ async function fetch(server: HapiServer) { return docs; } -export function getUsageCollector(server: HapiServer) { +export function getUsageCollector(taskManager: TaskManagerPluginSetupContract | undefined) { let isCollectorReady = false; async function determineIfTaskManagerIsReady() { let isReady = false; try { - isReady = await isTaskManagerReady(server); + isReady = await isTaskManagerReady(taskManager); } catch (err) {} // eslint-disable-line if (isReady) { @@ -60,7 +58,7 @@ export function getUsageCollector(server: HapiServer) { type: VIS_USAGE_TYPE, isReady: () => isCollectorReady, fetch: async () => { - const docs = await fetch(server); + const docs = await fetch(taskManager); // get the accumulated state from the recurring task return get(docs, '[0].state.stats'); }, diff --git a/x-pack/legacy/plugins/oss_telemetry/server/lib/collectors/visualizations/register_usage_collector.ts b/x-pack/legacy/plugins/oss_telemetry/server/lib/collectors/visualizations/register_usage_collector.ts index 09843a6f87ad7..1a47f68adcc58 100644 --- a/x-pack/legacy/plugins/oss_telemetry/server/lib/collectors/visualizations/register_usage_collector.ts +++ b/x-pack/legacy/plugins/oss_telemetry/server/lib/collectors/visualizations/register_usage_collector.ts @@ -5,13 +5,13 @@ */ import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; -import { HapiServer } from '../../../../'; +import { PluginSetupContract as TaskManagerPluginSetupContract } from '../../../../../task_manager/plugin'; import { getUsageCollector } from './get_usage_collector'; export function registerVisualizationsCollector( - usageCollection: UsageCollectionSetup, - server: HapiServer + collectorSet: UsageCollectionSetup, + taskManager: TaskManagerPluginSetupContract | undefined ): void { - const collector = usageCollection.makeUsageCollector(getUsageCollector(server)); - usageCollection.registerCollector(collector); + const collector = collectorSet.makeUsageCollector(getUsageCollector(taskManager)); + collectorSet.registerCollector(collector); } diff --git a/x-pack/legacy/plugins/oss_telemetry/server/lib/tasks/index.ts b/x-pack/legacy/plugins/oss_telemetry/server/lib/tasks/index.ts index 16e83a7938e60..cb6b4eab09741 100644 --- a/x-pack/legacy/plugins/oss_telemetry/server/lib/tasks/index.ts +++ b/x-pack/legacy/plugins/oss_telemetry/server/lib/tasks/index.ts @@ -4,15 +4,27 @@ * you may not use this file except in compliance with the Elastic License. */ -import { HapiServer } from '../../../'; +import { CoreSetup, Logger } from 'kibana/server'; +import { PluginSetupContract as TaskManagerPluginSetupContract } from '../../../../task_manager/plugin'; import { PLUGIN_ID, VIS_TELEMETRY_TASK } from '../../../constants'; import { visualizationsTaskRunner } from './visualizations/task_runner'; +import KbnServer from '../../../../../../../src/legacy/server/kbn_server'; +import { LegacyConfig } from '../../plugin'; +import { TaskInstance } from '../../../../task_manager'; -export function registerTasks(server: HapiServer) { - const taskManager = server.plugins.task_manager; - +export function registerTasks({ + taskManager, + logger, + elasticsearch, + config, +}: { + taskManager?: TaskManagerPluginSetupContract; + logger: Logger; + elasticsearch: CoreSetup['elasticsearch']; + config: LegacyConfig; +}) { if (!taskManager) { - server.log(['debug', 'telemetry'], `Task manager is not available`); + logger.debug('Task manager is not available'); return; } @@ -20,18 +32,30 @@ export function registerTasks(server: HapiServer) { [VIS_TELEMETRY_TASK]: { title: 'X-Pack telemetry calculator for Visualizations', type: VIS_TELEMETRY_TASK, - createTaskRunner({ taskInstance }: { taskInstance: any }) { + createTaskRunner({ taskInstance }: { taskInstance: TaskInstance }) { return { - run: visualizationsTaskRunner(taskInstance, server), + run: visualizationsTaskRunner(taskInstance, config, elasticsearch), }; }, }, }); } -export function scheduleTasks(server: HapiServer) { - const taskManager = server.plugins.task_manager; - const { kbnServer } = server.plugins.xpack_main.status.plugin; +export function scheduleTasks({ + taskManager, + xpackMainStatus, + logger, +}: { + taskManager?: TaskManagerPluginSetupContract; + xpackMainStatus: { kbnServer: KbnServer }; + logger: Logger; +}) { + if (!taskManager) { + logger.debug('Task manager is not available'); + return; + } + + const { kbnServer } = xpackMainStatus; kbnServer.afterPluginsInit(() => { // The code block below can't await directly within "afterPluginsInit" @@ -46,9 +70,10 @@ export function scheduleTasks(server: HapiServer) { id: `${PLUGIN_ID}-${VIS_TELEMETRY_TASK}`, taskType: VIS_TELEMETRY_TASK, state: { stats: {}, runs: 0 }, + params: {}, }); } catch (e) { - server.log(['debug', 'telemetry'], `Error scheduling task, received ${e.message}`); + logger.debug(`Error scheduling task, received ${e.message}`); } })(); }); diff --git a/x-pack/legacy/plugins/oss_telemetry/server/lib/tasks/visualizations/task_runner.test.ts b/x-pack/legacy/plugins/oss_telemetry/server/lib/tasks/visualizations/task_runner.test.ts index 5db08ed291d6d..0663a5bd330ca 100644 --- a/x-pack/legacy/plugins/oss_telemetry/server/lib/tasks/visualizations/task_runner.test.ts +++ b/x-pack/legacy/plugins/oss_telemetry/server/lib/tasks/visualizations/task_runner.test.ts @@ -5,28 +5,30 @@ */ import moment from 'moment'; -import { HapiServer, TaskInstance } from '../../../../'; import { getMockCallWithInternal, - getMockKbnServer, + getMockConfig, + getMockEs, getMockTaskInstance, } from '../../../../test_utils'; import { visualizationsTaskRunner } from './task_runner'; +import { TaskInstance } from '../../../../../task_manager'; describe('visualizationsTaskRunner', () => { let mockTaskInstance: TaskInstance; - let mockKbnServer: HapiServer; beforeEach(() => { mockTaskInstance = getMockTaskInstance(); - mockKbnServer = getMockKbnServer(); }); describe('Error handling', () => { test('catches its own errors', async () => { const mockCallWithInternal = () => Promise.reject(new Error('Things did not go well!')); - mockKbnServer = getMockKbnServer(mockCallWithInternal); - const runner = visualizationsTaskRunner(mockTaskInstance, mockKbnServer); + const runner = visualizationsTaskRunner( + mockTaskInstance, + getMockConfig(), + getMockEs(mockCallWithInternal) + ); const result = await runner(); expect(result).toMatchObject({ error: 'Things did not go well!', @@ -45,7 +47,7 @@ describe('visualizationsTaskRunner', () => { .startOf('day') .toDate(); - const runner = visualizationsTaskRunner(mockTaskInstance, mockKbnServer); + const runner = visualizationsTaskRunner(mockTaskInstance, getMockConfig(), getMockEs()); const result = await runner(); expect(result).toMatchObject({ @@ -123,9 +125,12 @@ describe('visualizationsTaskRunner', () => { }, }, ]); - mockKbnServer = getMockKbnServer(mockCallWithInternal); - const runner = visualizationsTaskRunner(mockTaskInstance, mockKbnServer); + const runner = visualizationsTaskRunner( + mockTaskInstance, + getMockConfig(), + getMockEs(mockCallWithInternal) + ); const result = await runner(); expect(result).toMatchObject({ diff --git a/x-pack/legacy/plugins/oss_telemetry/server/lib/tasks/visualizations/task_runner.ts b/x-pack/legacy/plugins/oss_telemetry/server/lib/tasks/visualizations/task_runner.ts index 3372101c2b457..9d8f76f6a10dc 100644 --- a/x-pack/legacy/plugins/oss_telemetry/server/lib/tasks/visualizations/task_runner.ts +++ b/x-pack/legacy/plugins/oss_telemetry/server/lib/tasks/visualizations/task_runner.ts @@ -5,15 +5,12 @@ */ import _, { countBy, groupBy, mapValues } from 'lodash'; -import { - ESQueryResponse, - HapiServer, - SavedObjectDoc, - TaskInstance, - VisState, - Visualization, -} from '../../../../'; +import { APICaller, CoreSetup } from 'kibana/server'; import { getNextMidnight } from '../../get_next_midnight'; +import { VisState } from '../../../../../../../../src/legacy/core_plugins/visualizations/public'; +import { TaskInstance } from '../../../../../task_manager'; +import { ESSearchHit } from '../../../../../apm/typings/elasticsearch'; +import { LegacyConfig } from '../../../plugin'; interface VisSummary { type: string; @@ -23,7 +20,7 @@ interface VisSummary { /* * Parse the response data into telemetry payload */ -async function getStats(callCluster: (method: string, params: any) => Promise, index: string) { +async function getStats(callCluster: APICaller, index: string) { const searchParams = { size: 10000, // elasticsearch index.max_result_window default value index, @@ -35,24 +32,26 @@ async function getStats(callCluster: (method: string, params: any) => Promise(esResponse, 'hits.hits.length'); if (size < 1) { return; } // `map` to get the raw types - const visSummaries: VisSummary[] = esResponse.hits.hits.map((hit: SavedObjectDoc) => { - const spacePhrases: string[] = hit._id.split(':'); - const space = spacePhrases.length === 3 ? spacePhrases[0] : 'default'; // if in a custom space, the format of a saved object ID is space:type:id - const visualization: Visualization = _.get(hit, '_source.visualization', { visState: '{}' }); - const visState: VisState = JSON.parse(visualization.visState); + const visSummaries: VisSummary[] = esResponse.hits.hits.map( + (hit: ESSearchHit<{ visState: string }>) => { + const spacePhrases: string[] = hit._id.split(':'); + const space = spacePhrases.length === 3 ? spacePhrases[0] : 'default'; // if in a custom space, the format of a saved object ID is space:type:id + const visualization = _.get(hit, '_source.visualization', { visState: '{}' }); + const visState: VisState = JSON.parse(visualization.visState); - return { - type: visState.type || '_na_', - space, - }; - }); + return { + type: visState.type || '_na_', + space, + }; + } + ); // organize stats per type const visTypes = groupBy(visSummaries, 'type'); @@ -72,9 +71,12 @@ async function getStats(callCluster: (method: string, params: any) => Promise { diff --git a/x-pack/legacy/plugins/oss_telemetry/server/plugin.ts b/x-pack/legacy/plugins/oss_telemetry/server/plugin.ts new file mode 100644 index 0000000000000..f661311fc24b8 --- /dev/null +++ b/x-pack/legacy/plugins/oss_telemetry/server/plugin.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CoreSetup, Logger, Plugin, PluginInitializerContext } from 'kibana/server'; +import { PluginSetupContract as TaskManagerPluginSetupContract } from '../../task_manager/plugin'; +import { registerCollectors } from './lib/collectors'; +import { registerTasks, scheduleTasks } from './lib/tasks'; +import KbnServer from '../../../../../src/legacy/server/kbn_server'; +import { UsageCollectionSetup } from '../../../../../src/plugins/usage_collection/server'; + +export interface LegacyConfig { + get: (key: string) => string | number | boolean; +} + +export interface OssTelemetrySetupDependencies { + usageCollection: UsageCollectionSetup; + __LEGACY: { + config: LegacyConfig; + xpackMainStatus: { kbnServer: KbnServer }; + }; + taskManager?: TaskManagerPluginSetupContract; +} + +export class OssTelemetryPlugin implements Plugin { + private logger: Logger; + + constructor(initializerContext: PluginInitializerContext) { + this.logger = initializerContext.logger.get(); + } + + public setup(core: CoreSetup, deps: OssTelemetrySetupDependencies) { + registerCollectors(deps); + registerTasks({ + taskManager: deps.taskManager, + logger: this.logger, + elasticsearch: core.elasticsearch, + config: deps.__LEGACY.config, + }); + scheduleTasks({ + taskManager: deps.taskManager, + xpackMainStatus: deps.__LEGACY.xpackMainStatus, + logger: this.logger, + }); + } + + public start() {} +} diff --git a/x-pack/legacy/plugins/oss_telemetry/test_utils/index.ts b/x-pack/legacy/plugins/oss_telemetry/test_utils/index.ts index 1cebe78b9c7f0..04e248d28b577 100644 --- a/x-pack/legacy/plugins/oss_telemetry/test_utils/index.ts +++ b/x-pack/legacy/plugins/oss_telemetry/test_utils/index.ts @@ -4,9 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ESQueryResponse, HapiServer, SavedObjectDoc, TaskInstance } from '../'; +import { APICaller, CoreSetup } from 'kibana/server'; -export const getMockTaskInstance = (): TaskInstance => ({ state: { runs: 0, stats: {} } }); +import { TaskInstance } from '../../task_manager'; +import { PluginSetupContract as TaskManagerPluginSetupContract } from '../../task_manager/plugin'; + +export const getMockTaskInstance = (): TaskInstance => ({ + state: { runs: 0, stats: {} }, + taskType: 'test', + params: {}, +}); const defaultMockSavedObjects = [ { @@ -20,10 +27,15 @@ const defaultMockSavedObjects = [ const defaultMockTaskDocs = [getMockTaskInstance()]; -export const getMockCallWithInternal = (hits: SavedObjectDoc[] = defaultMockSavedObjects) => { - return (): Promise => { +export const getMockEs = (mockCallWithInternal: APICaller = getMockCallWithInternal()) => + (({ + createClient: () => ({ callAsInternalUser: mockCallWithInternal }), + } as unknown) as CoreSetup['elasticsearch']); + +export const getMockCallWithInternal = (hits: unknown[] = defaultMockSavedObjects): APICaller => { + return ((() => { return Promise.resolve({ hits: { hits } }); - }; + }) as unknown) as APICaller; }; export const getMockTaskFetch = (docs: TaskInstance[] = defaultMockTaskDocs) => { @@ -36,24 +48,13 @@ export const getMockConfig = () => { }; }; -export const getMockKbnServer = ( - mockCallWithInternal = getMockCallWithInternal(), - mockTaskFetch = getMockTaskFetch(), - mockConfig = getMockConfig() -): HapiServer => ({ - plugins: { - elasticsearch: { - getCluster: (cluster: string) => ({ - callWithInternalUser: mockCallWithInternal, - }), - }, - xpack_main: {}, - task_manager: { - registerTaskDefinitions: (opts: any) => undefined, - ensureScheduled: (opts: any) => Promise.resolve(), - fetch: mockTaskFetch, - }, - }, - config: () => mockConfig, - log: () => undefined, +export const getMockTaskManager = (fetch: any = getMockTaskFetch()) => + (({ + registerTaskDefinitions: () => undefined, + ensureScheduled: () => Promise.resolve(), + fetch, + } as unknown) as TaskManagerPluginSetupContract); + +export const getCluster = () => ({ + callWithInternalUser: getMockCallWithInternal(), }); From 3c57f71c3ac2a781b49bf5cab252815d482a6599 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Tue, 10 Dec 2019 17:50:00 +0100 Subject: [PATCH 06/40] Removing stateful saved object finder (#52166) --- .../dashboard/dashboard_app_controller.tsx | 11 +- .../kibana/public/dashboard/legacy_imports.ts | 1 - .../open_search_panel.test.js.snap | 4 +- .../components/top_nav/open_search_panel.js | 11 +- .../top_nav/open_search_panel.test.js | 2 +- .../visualize_embeddable_factory.tsx | 3 +- .../kibana/public/visualize/legacy_imports.ts | 1 - .../visualize/listing/visualize_listing.html | 1 + .../visualize/listing/visualize_listing.js | 4 +- .../__snapshots__/new_vis_modal.test.tsx.snap | 2 + .../visualize/wizard/new_vis_modal.test.tsx | 8 ++ .../public/visualize/wizard/new_vis_modal.tsx | 10 +- .../search_selection/search_selection.tsx | 10 +- .../public/visualize/wizard/show_new_vis.tsx | 6 +- .../components/saved_object_finder.tsx | 108 ---------------- .../public/plugin.tsx | 2 +- .../saved_object_finder.test.tsx | 2 +- .../saved_objects/saved_object_finder.tsx | 28 ++++- .../public/np_ready/public/legacy.ts | 3 - .../public/np_ready/public/plugin.tsx | 15 ++- .../renderers/embeddable.tsx | 12 +- .../components/embeddable_flyout/flyout.tsx | 4 +- .../graph/public/components/source_picker.tsx | 4 +- .../new_job/pages/index_or_search/page.tsx | 7 +- .../components/embeddables/embedded_map.tsx | 9 +- .../search_selection/search_selection.tsx | 115 ++++++++++-------- .../legacy/plugins/transform/public/plugin.ts | 13 +- .../legacy/plugins/transform/public/shim.ts | 2 +- 28 files changed, 196 insertions(+), 202 deletions(-) delete mode 100644 src/legacy/ui/public/saved_objects/components/saved_object_finder.tsx diff --git a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app_controller.tsx b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app_controller.tsx index 3b336ebfc11fe..fd49b26e0d948 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app_controller.tsx +++ b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app_controller.tsx @@ -36,7 +36,6 @@ import { AppStateClass as TAppStateClass, KbnUrl, SaveOptions, - SavedObjectFinder, unhashUrl, } from './legacy_imports'; import { FilterStateManager, IndexPattern } from '../../../data/public'; @@ -70,6 +69,10 @@ import { DashboardAppScope } from './dashboard_app'; import { VISUALIZE_EMBEDDABLE_TYPE } from '../visualize/embeddable'; import { convertSavedDashboardPanelToPanelState } from './lib/embeddable_saved_object_converters'; import { RenderDeps } from './application'; +import { + SavedObjectFinderProps, + SavedObjectFinderUi, +} from '../../../../../plugins/kibana_react/public'; export interface DashboardAppControllerDependencies extends RenderDeps { $scope: DashboardAppScope; @@ -114,7 +117,7 @@ export class DashboardAppController { timefilter: { timefilter }, }, }, - core: { notifications, overlays, chrome, injectedMetadata }, + core: { notifications, overlays, chrome, injectedMetadata, uiSettings, savedObjects }, }: DashboardAppControllerDependencies) { new FilterStateManager(globalState, getAppState, filterManager); const queryFilter = filterManager; @@ -741,6 +744,10 @@ export class DashboardAppController { }; navActions[TopNavIds.ADD] = () => { if (dashboardContainer && !isErrorEmbeddable(dashboardContainer)) { + const SavedObjectFinder = (props: SavedObjectFinderProps) => ( + + ); + openAddPanelFlyout({ embeddable: dashboardContainer, getAllFactories: embeddables.getEmbeddableFactories, diff --git a/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts b/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts index b0f09f0cf9745..af0a833399a52 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts @@ -65,4 +65,3 @@ export { stateMonitorFactory, StateMonitor } from 'ui/state_management/state_mon export { ensureDefaultIndexPattern } from 'ui/legacy_compat'; export { unhashUrl } from '../../../../../plugins/kibana_utils/public'; export { IInjector } from 'ui/chrome'; -export { SavedObjectFinder } from 'ui/saved_objects/components/saved_object_finder'; diff --git a/src/legacy/core_plugins/kibana/public/discover/components/top_nav/__snapshots__/open_search_panel.test.js.snap b/src/legacy/core_plugins/kibana/public/discover/components/top_nav/__snapshots__/open_search_panel.test.js.snap index cc53e4bdcdcf9..2878b11040cf3 100644 --- a/src/legacy/core_plugins/kibana/public/discover/components/top_nav/__snapshots__/open_search_panel.test.js.snap +++ b/src/legacy/core_plugins/kibana/public/discover/components/top_nav/__snapshots__/open_search_panel.test.js.snap @@ -26,7 +26,7 @@ exports[`render 1`] = ` - diff --git a/src/legacy/core_plugins/kibana/public/discover/components/top_nav/open_search_panel.js b/src/legacy/core_plugins/kibana/public/discover/components/top_nav/open_search_panel.js index 0c3b52fbf0640..ec1763f44f25f 100644 --- a/src/legacy/core_plugins/kibana/public/discover/components/top_nav/open_search_panel.js +++ b/src/legacy/core_plugins/kibana/public/discover/components/top_nav/open_search_panel.js @@ -32,11 +32,16 @@ import { EuiFlyoutBody, EuiTitle, } from '@elastic/eui'; -import { SavedObjectFinder } from 'ui/saved_objects/components/saved_object_finder'; +import { SavedObjectFinderUi } from '../../../../../../../plugins/kibana_react/public'; +import { getServices } from '../../kibana_services'; const SEARCH_OBJECT_TYPE = 'search'; export function OpenSearchPanel(props) { + const { + core: { uiSettings, savedObjects }, + } = getServices(); + return ( @@ -50,7 +55,7 @@ export function OpenSearchPanel(props) { - diff --git a/src/legacy/core_plugins/kibana/public/discover/components/top_nav/open_search_panel.test.js b/src/legacy/core_plugins/kibana/public/discover/components/top_nav/open_search_panel.test.js index ea5c0ef39604d..0c82aeea95294 100644 --- a/src/legacy/core_plugins/kibana/public/discover/components/top_nav/open_search_panel.test.js +++ b/src/legacy/core_plugins/kibana/public/discover/components/top_nav/open_search_panel.test.js @@ -23,7 +23,7 @@ import { shallow } from 'enzyme'; jest.mock('../../kibana_services', () => { return { getServices: () => ({ - SavedObjectFinder: jest.fn() + core: { uiSettings: {}, savedObjects: {} }, }), }; }); diff --git a/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable_factory.tsx b/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable_factory.tsx index 7c9efa280c9f1..a377dafe9e512 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable_factory.tsx +++ b/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable_factory.tsx @@ -199,7 +199,8 @@ export class VisualizeEmbeddableFactory extends EmbeddableFactory< editorParams: ['addToDashboard'], }, npStart.core.http.basePath.prepend, - npStart.core.uiSettings + npStart.core.uiSettings, + npStart.core.savedObjects ); } return undefined; diff --git a/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts b/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts index 6adcfd2cc7186..b9909e522b571 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts @@ -45,7 +45,6 @@ export { PrivateProvider } from 'ui/private/private'; export { SavedObjectRegistryProvider } from 'ui/saved_objects'; export { SavedObjectSaveModal } from 'ui/saved_objects/components/saved_object_save_modal'; -export { SavedObjectFinder } from 'ui/saved_objects/components/saved_object_finder'; export { showSaveModal } from 'ui/saved_objects/show_saved_object_save_modal'; export { subscribeWithScope } from 'ui/utils/subscribe_with_scope'; diff --git a/src/legacy/core_plugins/kibana/public/visualize/listing/visualize_listing.html b/src/legacy/core_plugins/kibana/public/visualize/listing/visualize_listing.html index 4511ac61f7396..4ee8809fab228 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/listing/visualize_listing.html +++ b/src/legacy/core_plugins/kibana/public/visualize/listing/visualize_listing.html @@ -16,6 +16,7 @@ vis-types-registry="listingController.visTypeRegistry" add-base-path="listingController.addBasePath" ui-settings="listingController.uiSettings" + saved-objects="listingController.savedObjects" > diff --git a/src/legacy/core_plugins/kibana/public/visualize/listing/visualize_listing.js b/src/legacy/core_plugins/kibana/public/visualize/listing/visualize_listing.js index 9b02be0581b8d..b1ed5ce81d6ee 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/listing/visualize_listing.js +++ b/src/legacy/core_plugins/kibana/public/visualize/listing/visualize_listing.js @@ -34,6 +34,7 @@ export function initListingDirective(app) { ['onClose', { watchDepth: 'reference' }], ['addBasePath', { watchDepth: 'reference' }], ['uiSettings', { watchDepth: 'reference' }], + ['savedObjects', { watchDepth: 'reference' }], 'isOpen', ]) ); @@ -54,7 +55,7 @@ export function VisualizeListingController($injector, createNewVis) { toastNotifications, uiSettings, visualizations, - core: { docLinks }, + core: { docLinks, savedObjects }, } = getServices(); const kbnUrl = $injector.get('kbnUrl'); @@ -64,6 +65,7 @@ export function VisualizeListingController($injector, createNewVis) { this.showNewVisModal = false; this.addBasePath = addBasePath; this.uiSettings = uiSettings; + this.savedObjects = savedObjects; this.createNewVis = () => { this.showNewVisModal = true; diff --git a/src/legacy/core_plugins/kibana/public/visualize/wizard/__snapshots__/new_vis_modal.test.tsx.snap b/src/legacy/core_plugins/kibana/public/visualize/wizard/__snapshots__/new_vis_modal.test.tsx.snap index 5be5f58994887..04b7cddc75289 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/wizard/__snapshots__/new_vis_modal.test.tsx.snap +++ b/src/legacy/core_plugins/kibana/public/visualize/wizard/__snapshots__/new_vis_modal.test.tsx.snap @@ -108,6 +108,7 @@ exports[`NewVisModal filter for visualization types should render as expected 1` } isOpen={true} onClose={[Function]} + savedObjects={Object {}} uiSettings={ Object { "get": [MockFunction] { @@ -1413,6 +1414,7 @@ exports[`NewVisModal should render as expected 1`] = ` } isOpen={true} onClose={[Function]} + savedObjects={Object {}} uiSettings={ Object { "get": [MockFunction] { diff --git a/src/legacy/core_plugins/kibana/public/visualize/wizard/new_vis_modal.test.tsx b/src/legacy/core_plugins/kibana/public/visualize/wizard/new_vis_modal.test.tsx index 0dd2091bbfee0..4eafd06c7bb20 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/wizard/new_vis_modal.test.tsx +++ b/src/legacy/core_plugins/kibana/public/visualize/wizard/new_vis_modal.test.tsx @@ -29,6 +29,7 @@ jest.mock('../legacy_imports', () => ({ })); import { NewVisModal } from './new_vis_modal'; +import { SavedObjectsStart } from 'kibana/public'; describe('NewVisModal', () => { const defaultVisTypeParams = { @@ -76,6 +77,7 @@ describe('NewVisModal', () => { visTypesRegistry={visTypes} addBasePath={addBasePath} uiSettings={uiSettings} + savedObjects={{} as SavedObjectsStart} /> ); expect(wrapper).toMatchSnapshot(); @@ -89,6 +91,7 @@ describe('NewVisModal', () => { visTypesRegistry={visTypes} addBasePath={addBasePath} uiSettings={uiSettings} + savedObjects={{} as SavedObjectsStart} /> ); expect(wrapper.find('[data-test-subj="visType-vis"]').exists()).toBe(true); @@ -104,6 +107,7 @@ describe('NewVisModal', () => { visTypesRegistry={visTypes} addBasePath={addBasePath} uiSettings={uiSettings} + savedObjects={{} as SavedObjectsStart} /> ); const visButton = wrapper.find('button[data-test-subj="visType-vis"]'); @@ -121,6 +125,7 @@ describe('NewVisModal', () => { editorParams={['foo=true', 'bar=42']} addBasePath={addBasePath} uiSettings={uiSettings} + savedObjects={{} as SavedObjectsStart} /> ); const visButton = wrapper.find('button[data-test-subj="visType-vis"]'); @@ -138,6 +143,7 @@ describe('NewVisModal', () => { visTypesRegistry={visTypes} addBasePath={addBasePath} uiSettings={uiSettings} + savedObjects={{} as SavedObjectsStart} /> ); const searchBox = wrapper.find('input[data-test-subj="filterVisType"]'); @@ -156,6 +162,7 @@ describe('NewVisModal', () => { visTypesRegistry={visTypes} addBasePath={addBasePath} uiSettings={uiSettings} + savedObjects={{} as SavedObjectsStart} /> ); expect(wrapper.find('[data-test-subj="visType-visExp"]').exists()).toBe(false); @@ -170,6 +177,7 @@ describe('NewVisModal', () => { visTypesRegistry={visTypes} addBasePath={addBasePath} uiSettings={uiSettings} + savedObjects={{} as SavedObjectsStart} /> ); expect(wrapper.find('[data-test-subj="visType-visExp"]').exists()).toBe(true); diff --git a/src/legacy/core_plugins/kibana/public/visualize/wizard/new_vis_modal.tsx b/src/legacy/core_plugins/kibana/public/visualize/wizard/new_vis_modal.tsx index 0b46b562f2146..0402265610fb1 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/wizard/new_vis_modal.tsx +++ b/src/legacy/core_plugins/kibana/public/visualize/wizard/new_vis_modal.tsx @@ -22,7 +22,7 @@ import React from 'react'; import { EuiModal, EuiOverlayMask } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { IUiSettingsClient } from 'kibana/public'; +import { IUiSettingsClient, SavedObjectsStart } from 'kibana/public'; import { VisType } from '../legacy_imports'; import { VisualizeConstants } from '../visualize_constants'; import { createUiStatsReporter, METRIC_TYPE } from '../../../../ui_metric/public'; @@ -37,6 +37,7 @@ interface TypeSelectionProps { editorParams?: string[]; addBasePath: (path: string) => string; uiSettings: IUiSettingsClient; + savedObjects: SavedObjectsStart; } interface TypeSelectionState { @@ -81,7 +82,12 @@ class NewVisModal extends React.Component - + ) : ( void; visType: VisType; + uiSettings: IUiSettingsClient; + savedObjects: SavedObjectsStart; } export class SearchSelection extends React.Component { @@ -50,7 +54,7 @@ export class SearchSelection extends React.Component { - { }, ]} fixedPageSize={this.fixedPageSize} + uiSettings={this.props.uiSettings} + savedObjects={this.props.savedObjects} /> diff --git a/src/legacy/core_plugins/kibana/public/visualize/wizard/show_new_vis.tsx b/src/legacy/core_plugins/kibana/public/visualize/wizard/show_new_vis.tsx index 92320f7bb443a..88838e16c40e2 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/wizard/show_new_vis.tsx +++ b/src/legacy/core_plugins/kibana/public/visualize/wizard/show_new_vis.tsx @@ -21,7 +21,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { I18nProvider } from '@kbn/i18n/react'; -import { IUiSettingsClient } from 'kibana/public'; +import { IUiSettingsClient, SavedObjectsStart } from 'kibana/public'; import { NewVisModal } from './new_vis_modal'; import { TypesStart } from '../../../../visualizations/public/np_ready/public/types'; @@ -33,7 +33,8 @@ export function showNewVisModal( visTypeRegistry: TypesStart, { editorParams = [] }: ShowNewVisModalParams = {}, addBasePath: (path: string) => string, - uiSettings: IUiSettingsClient + uiSettings: IUiSettingsClient, + savedObjects: SavedObjectsStart ) { const container = document.createElement('div'); const onClose = () => { @@ -51,6 +52,7 @@ export function showNewVisModal( editorParams={editorParams} addBasePath={addBasePath} uiSettings={uiSettings} + savedObjects={savedObjects} /> ); diff --git a/src/legacy/ui/public/saved_objects/components/saved_object_finder.tsx b/src/legacy/ui/public/saved_objects/components/saved_object_finder.tsx deleted file mode 100644 index 5b787eb265509..0000000000000 --- a/src/legacy/ui/public/saved_objects/components/saved_object_finder.tsx +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React from 'react'; -import { npStart } from 'ui/new_platform'; -import { IconType } from '@elastic/eui'; -import { SavedObjectAttributes } from 'src/core/server'; -import { SimpleSavedObject } from 'src/core/public'; -import { SavedObjectFinder as SavedObjectFinderNP } from '../../../../../plugins/kibana_react/public'; - -/** - * DO NOT USE THIS COMPONENT, IT IS DEPRECATED. - * Use the one in `src/plugins/kibana_react` instead. - */ - -export interface SavedObjectMetaData { - type: string; - name: string; - getIconForSavedObject(savedObject: SimpleSavedObject): IconType; - getTooltipForSavedObject?(savedObject: SimpleSavedObject): string; - showSavedObject?(savedObject: SimpleSavedObject): boolean; -} - -interface BaseSavedObjectFinder { - /** - * @deprecated - * - * Use component in `src/plugins/kibana_react` instead. - */ - onChoose?: ( - id: SimpleSavedObject['id'], - type: SimpleSavedObject['type'], - name: string - ) => void; - /** - * @deprecated - * - * Use component in `src/plugins/kibana_react` instead. - */ - noItemsMessage?: React.ReactNode; - /** - * @deprecated - * - * Use component in `src/plugins/kibana_react` instead. - */ - savedObjectMetaData: Array>; - /** - * @deprecated - * - * Use component in `src/plugins/kibana_react` instead. - */ - showFilter?: boolean; -} - -interface SavedObjectFinderFixedPage extends BaseSavedObjectFinder { - /** - * @deprecated - * - * Use component in `src/plugins/kibana_react` instead. - */ - initialPageSize?: undefined; - /** - * @deprecated - * - * Use component in `src/plugins/kibana_react` instead. - */ - fixedPageSize: number; -} - -interface SavedObjectFinderInitialPageSize extends BaseSavedObjectFinder { - /** - * @deprecated - * - * Use component in `src/plugins/kibana_react` instead. - */ - initialPageSize?: 5 | 10 | 15 | 25; - /** - * @deprecated - * - * Use component in `src/plugins/kibana_react` instead. - */ - fixedPageSize?: undefined; -} -type SavedObjectFinderProps = SavedObjectFinderFixedPage | SavedObjectFinderInitialPageSize; - -export const SavedObjectFinder: React.FC = props => ( - -); diff --git a/src/plugins/dashboard_embeddable_container/public/plugin.tsx b/src/plugins/dashboard_embeddable_container/public/plugin.tsx index 79cc9b6980545..d18fbba239ec0 100644 --- a/src/plugins/dashboard_embeddable_container/public/plugin.tsx +++ b/src/plugins/dashboard_embeddable_container/public/plugin.tsx @@ -27,7 +27,7 @@ import { ExpandPanelAction, ReplacePanelAction } from '.'; import { DashboardContainerFactory } from './embeddable/dashboard_container_factory'; import { Start as InspectorStartContract } from '../../../plugins/inspector/public'; import { - SavedObjectFinder as SavedObjectFinderUi, + SavedObjectFinderUi, SavedObjectFinderProps, ExitFullScreenButton as ExitFullScreenButtonUi, ExitFullScreenButtonProps, diff --git a/src/plugins/kibana_react/public/saved_objects/saved_object_finder.test.tsx b/src/plugins/kibana_react/public/saved_objects/saved_object_finder.test.tsx index b35ba427378ab..58b396d57639b 100644 --- a/src/plugins/kibana_react/public/saved_objects/saved_object_finder.test.tsx +++ b/src/plugins/kibana_react/public/saved_objects/saved_object_finder.test.tsx @@ -35,7 +35,7 @@ import { IconType } from '@elastic/eui'; import { shallow } from 'enzyme'; import React from 'react'; import * as sinon from 'sinon'; -import { SavedObjectFinder } from './saved_object_finder'; +import { SavedObjectFinderUi as SavedObjectFinder } from './saved_object_finder'; // eslint-disable-next-line import { coreMock } from '../../../../core/public/mocks'; diff --git a/src/plugins/kibana_react/public/saved_objects/saved_object_finder.tsx b/src/plugins/kibana_react/public/saved_objects/saved_object_finder.tsx index c65d428958767..51fbbd2ba3046 100644 --- a/src/plugins/kibana_react/public/saved_objects/saved_object_finder.tsx +++ b/src/plugins/kibana_react/public/saved_objects/saved_object_finder.tsx @@ -46,6 +46,7 @@ import { i18n } from '@kbn/i18n'; import { SavedObjectAttributes } from '../../../../core/server'; import { SimpleSavedObject, CoreStart } from '../../../../core/public'; +import { useKibana } from '../context'; // TODO the typings for EuiListGroup are incorrect - maxWidth is missing. This can be removed when the types are adjusted const FixedEuiListGroup = (EuiListGroup as any) as React.FunctionComponent< @@ -104,12 +105,18 @@ interface SavedObjectFinderInitialPageSize extends BaseSavedObjectFinder { initialPageSize?: 5 | 10 | 15 | 25; fixedPageSize?: undefined; } -export type SavedObjectFinderProps = { + +export type SavedObjectFinderProps = SavedObjectFinderFixedPage | SavedObjectFinderInitialPageSize; + +export type SavedObjectFinderUiProps = { savedObjects: CoreStart['savedObjects']; uiSettings: CoreStart['uiSettings']; -} & (SavedObjectFinderFixedPage | SavedObjectFinderInitialPageSize); +} & SavedObjectFinderProps; -class SavedObjectFinder extends React.Component { +class SavedObjectFinderUi extends React.Component< + SavedObjectFinderUiProps, + SavedObjectFinderState +> { public static propTypes = { onChoose: PropTypes.func, noItemsMessage: PropTypes.node, @@ -174,7 +181,7 @@ class SavedObjectFinder extends React.Component { + const { services } = useKibana(); + return ( + + ); +}; + +export { SavedObjectFinder, SavedObjectFinderUi }; diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/legacy.ts b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/legacy.ts index a310403c86b5d..1928d7ac72313 100644 --- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/legacy.ts +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/legacy.ts @@ -22,7 +22,6 @@ import 'uiExports/embeddableFactories'; import 'uiExports/embeddableActions'; import { npSetup, npStart } from 'ui/new_platform'; -import { SavedObjectFinder } from 'ui/saved_objects/components/saved_object_finder'; import { ExitFullScreenButton } from 'ui/exit_full_screen'; import uiRoutes from 'ui/routes'; // @ts-ignore @@ -39,7 +38,6 @@ export const setup = pluginInstance.setup(npSetup.core, { embeddable: npSetup.plugins.embeddable, inspector: npSetup.plugins.inspector, __LEGACY: { - SavedObjectFinder, ExitFullScreenButton, }, }); @@ -64,7 +62,6 @@ export const start = pluginInstance.start(npStart.core, { inspector: npStart.plugins.inspector, uiActions: npStart.plugins.uiActions, __LEGACY: { - SavedObjectFinder, ExitFullScreenButton, onRenderComplete: (renderCompleteListener: () => void) => { if (rendered) { diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/plugin.tsx b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/plugin.tsx index 6b82a67b9fcda..adf898d9af2c7 100644 --- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/plugin.tsx +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/plugin.tsx @@ -38,6 +38,10 @@ import { ContactCardEmbeddableFactory, } from './embeddable_api'; import { App } from './app'; +import { + SavedObjectFinderProps, + SavedObjectFinderUi, +} from '../../../../../../../src/plugins/kibana_react/public/saved_objects'; import { IEmbeddableStart, IEmbeddableSetup, @@ -47,7 +51,6 @@ export interface SetupDependencies { embeddable: IEmbeddableSetup; inspector: InspectorSetupContract; __LEGACY: { - SavedObjectFinder: React.ComponentType; ExitFullScreenButton: React.ComponentType; }; } @@ -57,7 +60,6 @@ interface StartDependencies { uiActions: IUiActionsStart; inspector: InspectorStartContract; __LEGACY: { - SavedObjectFinder: React.ComponentType; ExitFullScreenButton: React.ComponentType; onRenderComplete: (onRenderComplete: () => void) => void; }; @@ -99,6 +101,13 @@ export class EmbeddableExplorerPublicPlugin plugins.__LEGACY.onRenderComplete(() => { const root = document.getElementById(REACT_ROOT_ID); + const SavedObjectFinder = (props: SavedObjectFinderProps) => ( + + ); ReactDOM.render( , root diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable.tsx b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable.tsx index 8810871e9161b..5c7ef1a8c1799 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable.tsx +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable.tsx @@ -16,8 +16,11 @@ import { } from '../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public'; import { start } from '../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public/legacy'; import { EmbeddableExpression } from '../expression_types/embeddable'; -import { SavedObjectFinder } from '../../../../../../src/legacy/ui/public/saved_objects/components/saved_object_finder'; import { RendererStrings } from '../../i18n'; +import { + SavedObjectFinderProps, + SavedObjectFinderUi, +} from '../../../../../../src/plugins/kibana_react/public'; const { embeddable: strings } = RendererStrings; @@ -34,6 +37,13 @@ interface Handlers { } const renderEmbeddable = (embeddableObject: IEmbeddable, domNode: HTMLElement) => { + const SavedObjectFinder = (props: SavedObjectFinderProps) => ( + + ); return (
{ - { diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/index_or_search/page.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/index_or_search/page.tsx index 68013bd243a91..cb311f04dd1d7 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/index_or_search/page.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/index_or_search/page.tsx @@ -15,7 +15,8 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { SavedObjectFinder } from 'ui/saved_objects/components/saved_object_finder'; +import { npStart } from 'ui/new_platform'; +import { SavedObjectFinderUi } from '../../../../../../../../../../src/plugins/kibana_react/public'; export interface PageProps { nextStepPath: string; @@ -46,7 +47,7 @@ export const Page: FC = ({ nextStepPath }) => { - = ({ nextStepPath }) => { }, ]} fixedPageSize={RESULTS_PER_PAGE} + uiSettings={npStart.core.uiSettings} + savedObjects={npStart.core.savedObjects} /> diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map.tsx b/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map.tsx index cb73cf73b8d06..1658002408fb0 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map.tsx +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map.tsx @@ -9,7 +9,6 @@ import React, { useEffect, useState } from 'react'; import { createPortalNode, InPortal } from 'react-reverse-portal'; import styled, { css } from 'styled-components'; import { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } from 'ui/documentation_links'; -import { SavedObjectFinder } from 'ui/saved_objects/components/saved_object_finder'; import { EmbeddablePanel } from '../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public'; import { start } from '../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public/legacy'; @@ -29,6 +28,10 @@ import { MapToolTip } from './map_tool_tip/map_tool_tip'; import * as i18n from './translations'; import { MapEmbeddable, SetQuery } from './types'; import { Query, esFilters } from '../../../../../../../src/plugins/data/public'; +import { + SavedObjectFinderProps, + SavedObjectFinderUi, +} from '../../../../../../../src/plugins/kibana_react/public'; interface EmbeddableMapProps { maintainRatio?: boolean; @@ -176,6 +179,10 @@ export const EmbeddedMapComponent = ({ } }, [startDate, endDate]); + const SavedObjectFinder = (props: SavedObjectFinderProps) => ( + + ); + return isError ? null : ( diff --git a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/search_selection/search_selection.tsx b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/search_selection/search_selection.tsx index 1a270505d61a6..368c5aa806fe8 100644 --- a/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/search_selection/search_selection.tsx +++ b/x-pack/legacy/plugins/transform/public/app/sections/transform_management/components/search_selection/search_selection.tsx @@ -8,8 +8,8 @@ import { EuiModalBody, EuiModalHeader, EuiModalHeaderTitle } from '@elastic/eui' import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { FC } from 'react'; - -import { SavedObjectFinder } from 'ui/saved_objects/components/saved_object_finder'; +import { SavedObjectFinderUi } from '../../../../../../../../../../src/plugins/kibana_react/public'; +import { useAppDependencies } from '../../../../app_dependencies'; interface SearchSelectionProps { onSearchSelected: (searchId: string, searchType: string) => void; @@ -17,56 +17,63 @@ interface SearchSelectionProps { const fixedPageSize: number = 8; -export const SearchSelection: FC = ({ onSearchSelected }) => ( - <> - - - {' '} - /{' '} - = ({ onSearchSelected }) => { + const { + core: { uiSettings, savedObjects }, + } = useAppDependencies(); + return ( + <> + + + {' '} + /{' '} + + + + + 'search', + name: i18n.translate( + 'xpack.transform.newTransform.searchSelection.savedObjectType.search', + { + defaultMessage: 'Saved search', + } + ), + }, + { + type: 'index-pattern', + getIconForSavedObject: () => 'indexPatternApp', + name: i18n.translate( + 'xpack.transform.newTransform.searchSelection.savedObjectType.indexPattern', + { + defaultMessage: 'Index pattern', + } + ), + }, + ]} + fixedPageSize={fixedPageSize} + uiSettings={uiSettings} + savedObjects={savedObjects} /> - - - - 'search', - name: i18n.translate( - 'xpack.transform.newTransform.searchSelection.savedObjectType.search', - { - defaultMessage: 'Saved search', - } - ), - }, - { - type: 'index-pattern', - getIconForSavedObject: () => 'indexPatternApp', - name: i18n.translate( - 'xpack.transform.newTransform.searchSelection.savedObjectType.indexPattern', - { - defaultMessage: 'Index pattern', - } - ), - }, - ]} - fixedPageSize={fixedPageSize} - /> - - -); + + + ); +}; diff --git a/x-pack/legacy/plugins/transform/public/plugin.ts b/x-pack/legacy/plugins/transform/public/plugin.ts index e7cc83d16b3b9..08a3a06fc24fc 100644 --- a/x-pack/legacy/plugins/transform/public/plugin.ts +++ b/x-pack/legacy/plugins/transform/public/plugin.ts @@ -27,12 +27,21 @@ const template = `
; +export type AppCore = Pick; export interface AppPlugins { management: { From cf28280496c76e58a1b937d16add9aab6350155b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20St=C3=BCrmer?= Date: Tue, 10 Dec 2019 19:21:10 +0100 Subject: [PATCH 07/40] [Logs UI] Generalize ML module management (#50662) This abstracts the specific job details out of the ML module management hooks to enable re-use with the upcoming categorization module. closes #50322 --- .../log_analysis/results/log_entry_rate.ts | 12 +- .../http_api/log_analysis/validation/index.ts | 2 +- .../{indices.ts => log_entry_rate_indices.ts} | 14 +- .../common/log_analysis/job_parameters.ts | 18 +- .../analyze_in_ml_button.tsx | 9 +- .../logging/log_analysis_results/index.ts | 7 + .../logs/log_analysis/api/ml_api_types.ts | 27 ++- .../logs/log_analysis/api/ml_cleanup.ts | 39 +++- .../api/ml_get_jobs_summary_api.ts | 10 +- .../log_analysis/api/ml_setup_module_api.ts | 47 ++--- ...tterns_validate.ts => validate_indices.ts} | 14 +- .../containers/logs/log_analysis/index.ts | 7 +- .../log_analysis_capabilities.tsx | 2 +- .../log_analysis/log_analysis_cleanup.tsx | 82 +++----- .../logs/log_analysis/log_analysis_jobs.tsx | 169 ---------------- .../logs/log_analysis/log_analysis_module.tsx | 167 +++++++++++++++ ...ate.tsx => log_analysis_module_status.tsx} | 190 ++++++++++-------- .../log_analysis/log_analysis_module_types.ts | 36 ++++ .../log_analysis/log_analysis_setup_state.tsx | 92 +++++---- .../logs/log_analysis/log_entry_rate.tsx | 50 ----- .../logs/log_analysis/ml_api_types.ts | 28 --- .../infra/public/containers/source/index.ts | 2 +- .../infra/public/containers/source/source.tsx | 1 + .../plugins/infra/public/pages/logs/index.tsx | 4 +- .../first_use.tsx | 0 .../{analysis => log_entry_rate}/index.ts | 0 .../logs/log_entry_rate/module_descriptor.ts | 107 ++++++++++ .../{analysis => log_entry_rate}/page.tsx | 14 +- .../page_content.tsx | 39 ++-- .../page_providers.tsx | 16 +- .../page_results_content.tsx | 79 ++++---- .../page_setup_content.tsx | 46 +++-- .../page_setup_status_unknown.tsx | 2 +- .../page_unavailable_content.tsx | 2 +- .../sections/anomalies/chart.tsx | 0 .../sections/anomalies/expanded_row.tsx | 25 ++- .../sections/anomalies/index.tsx | 6 +- .../sections/anomalies/table.tsx | 4 +- .../sections/helpers/data_formatters.tsx | 26 ++- .../sections/log_rate/bar_chart.tsx | 0 .../sections/log_rate/index.tsx | 2 +- .../service_calls}/get_log_entry_rate.ts | 0 .../setup/index.ts | 0 .../analysis_setup_indices_form.tsx | 17 +- .../analysis_setup_timerange_form.tsx | 0 .../setup/initial_configuration_step/index.ts | 0 .../initial_configuration_step.tsx | 0 .../process_step/create_ml_jobs_button.tsx | 0 .../setup/process_step/index.ts | 0 .../setup/process_step/process_step.tsx | 0 .../process_step/recreate_ml_jobs_button.tsx | 0 .../setup/setup_steps.tsx | 24 ++- .../use_log_entry_rate_module.tsx | 45 +++++ .../use_log_entry_rate_results.ts} | 78 +++---- .../use_log_entry_rate_results_url_state.tsx} | 7 +- .../plugins/infra/server/infra_server.ts | 8 +- .../lib/adapters/framework/adapter_types.ts | 4 +- .../infra/server/routes/log_analysis/index.ts | 2 +- .../log_analysis/results/log_entry_rate.ts | 5 +- .../{index_patterns => validation}/index.ts | 2 +- .../validate.ts => validation/indices.ts} | 25 +-- 61 files changed, 910 insertions(+), 704 deletions(-) rename x-pack/legacy/plugins/infra/common/http_api/log_analysis/validation/{indices.ts => log_entry_rate_indices.ts} (74%) rename x-pack/legacy/plugins/infra/public/{pages/logs/analysis/sections => components/logging/log_analysis_results}/analyze_in_ml_button.tsx (96%) create mode 100644 x-pack/legacy/plugins/infra/public/components/logging/log_analysis_results/index.ts rename x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/{index_patterns_validate.ts => validate_indices.ts} (70%) delete mode 100644 x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_jobs.tsx create mode 100644 x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_module.tsx rename x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/{log_analysis_status_state.tsx => log_analysis_module_status.tsx} (70%) create mode 100644 x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_module_types.ts delete mode 100644 x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_entry_rate.tsx delete mode 100644 x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/ml_api_types.ts rename x-pack/legacy/plugins/infra/public/pages/logs/{analysis => log_entry_rate}/first_use.tsx (100%) rename x-pack/legacy/plugins/infra/public/pages/logs/{analysis => log_entry_rate}/index.ts (100%) create mode 100644 x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/module_descriptor.ts rename x-pack/legacy/plugins/infra/public/pages/logs/{analysis => log_entry_rate}/page.tsx (53%) rename x-pack/legacy/plugins/infra/public/pages/logs/{analysis => log_entry_rate}/page_content.tsx (59%) rename x-pack/legacy/plugins/infra/public/pages/logs/{analysis => log_entry_rate}/page_providers.tsx (53%) rename x-pack/legacy/plugins/infra/public/pages/logs/{analysis => log_entry_rate}/page_results_content.tsx (78%) rename x-pack/legacy/plugins/infra/public/pages/logs/{analysis => log_entry_rate}/page_setup_content.tsx (70%) rename x-pack/legacy/plugins/infra/public/pages/logs/{analysis => log_entry_rate}/page_setup_status_unknown.tsx (92%) rename x-pack/legacy/plugins/infra/public/pages/logs/{analysis => log_entry_rate}/page_unavailable_content.tsx (95%) rename x-pack/legacy/plugins/infra/public/pages/logs/{analysis => log_entry_rate}/sections/anomalies/chart.tsx (100%) rename x-pack/legacy/plugins/infra/public/pages/logs/{analysis => log_entry_rate}/sections/anomalies/expanded_row.tsx (84%) rename x-pack/legacy/plugins/infra/public/pages/logs/{analysis => log_entry_rate}/sections/anomalies/index.tsx (97%) rename x-pack/legacy/plugins/infra/public/pages/logs/{analysis => log_entry_rate}/sections/anomalies/table.tsx (97%) rename x-pack/legacy/plugins/infra/public/pages/logs/{analysis => log_entry_rate}/sections/helpers/data_formatters.tsx (88%) rename x-pack/legacy/plugins/infra/public/pages/logs/{analysis => log_entry_rate}/sections/log_rate/bar_chart.tsx (100%) rename x-pack/legacy/plugins/infra/public/pages/logs/{analysis => log_entry_rate}/sections/log_rate/index.tsx (96%) rename x-pack/legacy/plugins/infra/public/{containers/logs/log_analysis/api => pages/logs/log_entry_rate/service_calls}/get_log_entry_rate.ts (100%) rename x-pack/legacy/plugins/infra/public/pages/logs/{analysis => log_entry_rate}/setup/index.ts (100%) rename x-pack/legacy/plugins/infra/public/pages/logs/{analysis => log_entry_rate}/setup/initial_configuration_step/analysis_setup_indices_form.tsx (91%) rename x-pack/legacy/plugins/infra/public/pages/logs/{analysis => log_entry_rate}/setup/initial_configuration_step/analysis_setup_timerange_form.tsx (100%) rename x-pack/legacy/plugins/infra/public/pages/logs/{analysis => log_entry_rate}/setup/initial_configuration_step/index.ts (100%) rename x-pack/legacy/plugins/infra/public/pages/logs/{analysis => log_entry_rate}/setup/initial_configuration_step/initial_configuration_step.tsx (100%) rename x-pack/legacy/plugins/infra/public/pages/logs/{analysis => log_entry_rate}/setup/process_step/create_ml_jobs_button.tsx (100%) rename x-pack/legacy/plugins/infra/public/pages/logs/{analysis => log_entry_rate}/setup/process_step/index.ts (100%) rename x-pack/legacy/plugins/infra/public/pages/logs/{analysis => log_entry_rate}/setup/process_step/process_step.tsx (100%) rename x-pack/legacy/plugins/infra/public/pages/logs/{analysis => log_entry_rate}/setup/process_step/recreate_ml_jobs_button.tsx (100%) rename x-pack/legacy/plugins/infra/public/pages/logs/{analysis => log_entry_rate}/setup/setup_steps.tsx (84%) create mode 100644 x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_rate_module.tsx rename x-pack/legacy/plugins/infra/public/{containers/logs/log_analysis/log_analysis_results.tsx => pages/logs/log_entry_rate/use_log_entry_rate_results.ts} (58%) rename x-pack/legacy/plugins/infra/public/{containers/logs/log_analysis/log_analysis_results_url_state.tsx => pages/logs/log_entry_rate/use_log_entry_rate_results_url_state.tsx} (96%) rename x-pack/legacy/plugins/infra/server/routes/log_analysis/{index_patterns => validation}/index.ts (89%) rename x-pack/legacy/plugins/infra/server/routes/log_analysis/{index_patterns/validate.ts => validation/indices.ts} (77%) diff --git a/x-pack/legacy/plugins/infra/common/http_api/log_analysis/results/log_entry_rate.ts b/x-pack/legacy/plugins/infra/common/http_api/log_analysis/results/log_entry_rate.ts index 5a1412fd8f3d4..dfc3d2aabd11a 100644 --- a/x-pack/legacy/plugins/infra/common/http_api/log_analysis/results/log_entry_rate.ts +++ b/x-pack/legacy/plugins/infra/common/http_api/log_analysis/results/log_entry_rate.ts @@ -29,7 +29,7 @@ export type GetLogEntryRateRequestPayload = rt.TypeOf; + +export const logEntryRateHistogramBucketRT = rt.type({ partitions: rt.array(logEntryRatePartitionRT), startTime: rt.number, }); +export type LogEntryRateHistogramBucket = rt.TypeOf; + export const getLogEntryRateSuccessReponsePayloadRT = rt.type({ data: rt.type({ bucketDuration: rt.number, - histogramBuckets: rt.array(logEntryRateHistogramBucket), + histogramBuckets: rt.array(logEntryRateHistogramBucketRT), totalNumberOfLogEntries: rt.number, }), }); diff --git a/x-pack/legacy/plugins/infra/common/http_api/log_analysis/validation/index.ts b/x-pack/legacy/plugins/infra/common/http_api/log_analysis/validation/index.ts index 727faca69298e..f23ef7ee7c302 100644 --- a/x-pack/legacy/plugins/infra/common/http_api/log_analysis/validation/index.ts +++ b/x-pack/legacy/plugins/infra/common/http_api/log_analysis/validation/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './indices'; +export * from './log_entry_rate_indices'; diff --git a/x-pack/legacy/plugins/infra/common/http_api/log_analysis/validation/indices.ts b/x-pack/legacy/plugins/infra/common/http_api/log_analysis/validation/log_entry_rate_indices.ts similarity index 74% rename from x-pack/legacy/plugins/infra/common/http_api/log_analysis/validation/indices.ts rename to x-pack/legacy/plugins/infra/common/http_api/log_analysis/validation/log_entry_rate_indices.ts index 62d81dc136853..5b2509074f6ed 100644 --- a/x-pack/legacy/plugins/infra/common/http_api/log_analysis/validation/indices.ts +++ b/x-pack/legacy/plugins/infra/common/http_api/log_analysis/validation/log_entry_rate_indices.ts @@ -6,14 +6,24 @@ import * as rt from 'io-ts'; -export const LOG_ANALYSIS_VALIDATION_INDICES_PATH = '/api/infra/log_analysis/validation/indices'; +export const LOG_ANALYSIS_VALIDATE_INDICES_PATH = + '/api/infra/log_analysis/validation/log_entry_rate_indices'; /** * Request types */ +export const validationIndicesFieldSpecificationRT = rt.type({ + name: rt.string, + validTypes: rt.array(rt.string), +}); + +export type ValidationIndicesFieldSpecification = rt.TypeOf< + typeof validationIndicesFieldSpecificationRT +>; + export const validationIndicesRequestPayloadRT = rt.type({ data: rt.type({ - timestampField: rt.string, + fields: rt.array(validationIndicesFieldSpecificationRT), indices: rt.array(rt.string), }), }); diff --git a/x-pack/legacy/plugins/infra/common/log_analysis/job_parameters.ts b/x-pack/legacy/plugins/infra/common/log_analysis/job_parameters.ts index 5cfe38394a2ce..626e90b65a7d8 100644 --- a/x-pack/legacy/plugins/infra/common/log_analysis/job_parameters.ts +++ b/x-pack/legacy/plugins/infra/common/log_analysis/job_parameters.ts @@ -4,19 +4,25 @@ * you may not use this file except in compliance with the Elastic License. */ -import { JobType } from './log_analysis'; +import * as rt from 'io-ts'; export const bucketSpan = 900000; +export const partitionField = 'event.dataset'; + export const getJobIdPrefix = (spaceId: string, sourceId: string) => `kibana-logs-ui-${spaceId}-${sourceId}-`; -export const getJobId = (spaceId: string, sourceId: string, jobType: JobType) => +export const getJobId = (spaceId: string, sourceId: string, jobType: string) => `${getJobIdPrefix(spaceId, sourceId)}${jobType}`; -export const getDatafeedId = (spaceId: string, sourceId: string, jobType: JobType) => +export const getDatafeedId = (spaceId: string, sourceId: string, jobType: string) => `datafeed-${getJobId(spaceId, sourceId, jobType)}`; -export const getAllModuleJobIds = (spaceId: string, sourceId: string) => [ - getJobId(spaceId, sourceId, 'log-entry-rate'), -]; +export const jobSourceConfigurationRT = rt.type({ + indexPattern: rt.string, + timestampField: rt.string, + bucketSpan: rt.number, +}); + +export type JobSourceConfiguration = rt.TypeOf; diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/analyze_in_ml_button.tsx b/x-pack/legacy/plugins/infra/public/components/logging/log_analysis_results/analyze_in_ml_button.tsx similarity index 96% rename from x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/analyze_in_ml_button.tsx rename to x-pack/legacy/plugins/infra/public/components/logging/log_analysis_results/analyze_in_ml_button.tsx index ef81f229034bd..c5d83e1c205cc 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/analyze_in_ml_button.tsx +++ b/x-pack/legacy/plugins/infra/public/components/logging/log_analysis_results/analyze_in_ml_button.tsx @@ -4,14 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import url from 'url'; import { EuiButton } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import React from 'react'; +import { encode } from 'rison-node'; import chrome from 'ui/chrome'; import { QueryString } from 'ui/utils/query_string'; -import { encode } from 'rison-node'; -import { TimeRange } from '../../../../../common/http_api/shared/time_range'; +import url from 'url'; + +import { TimeRange } from '../../../../common/http_api/shared/time_range'; export const AnalyzeInMlButton: React.FunctionComponent<{ jobId: string; diff --git a/x-pack/legacy/plugins/infra/public/components/logging/log_analysis_results/index.ts b/x-pack/legacy/plugins/infra/public/components/logging/log_analysis_results/index.ts new file mode 100644 index 0000000000000..8a4ceb70252a3 --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/components/logging/log_analysis_results/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './analyze_in_ml_button'; diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/ml_api_types.ts b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/ml_api_types.ts index deb3d528e42c2..9d4d419ceebe3 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/ml_api_types.ts +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/ml_api_types.ts @@ -6,11 +6,30 @@ import * as rt from 'io-ts'; +import { jobSourceConfigurationRT } from '../../../../../common/log_analysis'; + export const jobCustomSettingsRT = rt.partial({ job_revision: rt.number, - logs_source_config: rt.partial({ - indexPattern: rt.string, - timestampField: rt.string, - bucketSpan: rt.number, + logs_source_config: rt.partial(jobSourceConfigurationRT.props), +}); + +export const getMlCapabilitiesResponsePayloadRT = rt.type({ + capabilities: rt.type({ + canGetJobs: rt.boolean, + canCreateJob: rt.boolean, + canDeleteJob: rt.boolean, + canOpenJob: rt.boolean, + canCloseJob: rt.boolean, + canForecastJob: rt.boolean, + canGetDatafeeds: rt.boolean, + canStartStopDatafeed: rt.boolean, + canUpdateJob: rt.boolean, + canUpdateDatafeed: rt.boolean, + canPreviewDatafeed: rt.boolean, }), + isPlatinumOrTrialLicense: rt.boolean, + mlFeatureEnabledInSpace: rt.boolean, + upgradeInProgress: rt.boolean, }); + +export type GetMlCapabilitiesResponsePayload = rt.TypeOf; diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/ml_cleanup.ts b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/ml_cleanup.ts index 209da920c4c8b..5054f607fa5dc 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/ml_cleanup.ts +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/ml_cleanup.ts @@ -9,17 +9,22 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; import { kfetch } from 'ui/kfetch'; -import { getAllModuleJobIds, getDatafeedId } from '../../../../../common/log_analysis'; + +import { getDatafeedId, getJobId } from '../../../../../common/log_analysis'; import { throwErrors, createPlainError } from '../../../../../common/runtime_types'; -export const callDeleteJobs = async (spaceId: string, sourceId: string) => { +export const callDeleteJobs = async ( + spaceId: string, + sourceId: string, + jobTypes: JobType[] +) => { // NOTE: Deleting the jobs via this API will delete the datafeeds at the same time const deleteJobsResponse = await kfetch({ method: 'POST', pathname: '/api/ml/jobs/delete_jobs', body: JSON.stringify( deleteJobsRequestPayloadRT.encode({ - jobIds: getAllModuleJobIds(spaceId, sourceId), + jobIds: jobTypes.map(jobType => getJobId(spaceId, sourceId, jobType)), }) ), }); @@ -42,15 +47,24 @@ export const callGetJobDeletionTasks = async () => { ); }; -export const callStopDatafeed = async (spaceId: string, sourceId: string) => { +export const callStopDatafeeds = async ( + spaceId: string, + sourceId: string, + jobTypes: JobType[] +) => { // Stop datafeed due to https://github.com/elastic/kibana/issues/44652 const stopDatafeedResponse = await kfetch({ method: 'POST', - pathname: `/api/ml/datafeeds/${getDatafeedId(spaceId, sourceId, 'log-entry-rate')}/_stop`, + pathname: '/api/ml/jobs/stop_datafeeds', + body: JSON.stringify( + stopDatafeedsRequestPayloadRT.encode({ + datafeedIds: jobTypes.map(jobType => getDatafeedId(spaceId, sourceId, jobType)), + }) + ), }); return pipe( - stopDatafeedResponsePayloadRT.decode(stopDatafeedResponse), + stopDatafeedsResponsePayloadRT.decode(stopDatafeedResponse), fold(throwErrors(createPlainError), identity) ); }; @@ -68,10 +82,19 @@ export const deleteJobsResponsePayloadRT = rt.record( }) ); +export type DeleteJobsResponsePayload = rt.TypeOf; + export const getJobDeletionTasksResponsePayloadRT = rt.type({ jobIds: rt.array(rt.string), }); -export const stopDatafeedResponsePayloadRT = rt.type({ - stopped: rt.boolean, +export const stopDatafeedsRequestPayloadRT = rt.type({ + datafeedIds: rt.array(rt.string), }); + +export const stopDatafeedsResponsePayloadRT = rt.record( + rt.string, + rt.type({ + stopped: rt.boolean, + }) +); diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/ml_get_jobs_summary_api.ts b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/ml_get_jobs_summary_api.ts index 6171d10b5f1aa..91e517b0db008 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/ml_get_jobs_summary_api.ts +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/ml_get_jobs_summary_api.ts @@ -12,15 +12,19 @@ import { kfetch } from 'ui/kfetch'; import { jobCustomSettingsRT } from './ml_api_types'; import { throwErrors, createPlainError } from '../../../../../common/runtime_types'; -import { getAllModuleJobIds } from '../../../../../common/log_analysis'; +import { getJobId } from '../../../../../common/log_analysis'; -export const callJobsSummaryAPI = async (spaceId: string, sourceId: string) => { +export const callJobsSummaryAPI = async ( + spaceId: string, + sourceId: string, + jobTypes: JobType[] +) => { const response = await kfetch({ method: 'POST', pathname: '/api/ml/jobs/jobs_summary', body: JSON.stringify( fetchJobStatusRequestPayloadRT.encode({ - jobIds: getAllModuleJobIds(spaceId, sourceId), + jobIds: jobTypes.map(jobType => getJobId(spaceId, sourceId, jobType)), }) ), }); diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/ml_setup_module_api.ts b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/ml_setup_module_api.ts index 1c937513c7950..80a4f975cdd57 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/ml_setup_module_api.ts +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/ml_setup_module_api.ts @@ -12,7 +12,6 @@ import { kfetch } from 'ui/kfetch'; import { throwErrors, createPlainError } from '../../../../../common/runtime_types'; import { getJobIdPrefix } from '../../../../../common/log_analysis'; -import { jobCustomSettingsRT } from './ml_api_types'; export const callSetupMlModuleAPI = async ( moduleId: string, @@ -21,8 +20,8 @@ export const callSetupMlModuleAPI = async ( spaceId: string, sourceId: string, indexPattern: string, - timeField: string, - bucketSpan: number + jobOverrides: SetupMlModuleJobOverrides[] = [], + datafeedOverrides: SetupMlModuleDatafeedOverrides[] = [] ) => { const response = await kfetch({ method: 'POST', @@ -34,25 +33,8 @@ export const callSetupMlModuleAPI = async ( indexPatternName: indexPattern, prefix: getJobIdPrefix(spaceId, sourceId), startDatafeed: true, - jobOverrides: [ - { - job_id: 'log-entry-rate' as const, - analysis_config: { - bucket_span: `${bucketSpan}ms`, - }, - data_description: { - time_field: timeField, - }, - custom_settings: { - logs_source_config: { - indexPattern, - timestampField: timeField, - bucketSpan, - }, - }, - }, - ], - datafeedOverrides: [], + jobOverrides, + datafeedOverrides, }) ), }); @@ -68,23 +50,20 @@ const setupMlModuleTimeParamsRT = rt.partial({ end: rt.number, }); -const setupMlModuleLogEntryRateJobOverridesRT = rt.type({ - job_id: rt.literal('log-entry-rate'), - analysis_config: rt.type({ - bucket_span: rt.string, - }), - data_description: rt.type({ - time_field: rt.string, - }), - custom_settings: jobCustomSettingsRT, -}); +const setupMlModuleJobOverridesRT = rt.object; + +export type SetupMlModuleJobOverrides = rt.TypeOf; + +const setupMlModuleDatafeedOverridesRT = rt.object; + +export type SetupMlModuleDatafeedOverrides = rt.TypeOf; const setupMlModuleRequestParamsRT = rt.type({ indexPatternName: rt.string, prefix: rt.string, startDatafeed: rt.boolean, - jobOverrides: rt.array(setupMlModuleLogEntryRateJobOverridesRT), - datafeedOverrides: rt.array(rt.object), + jobOverrides: rt.array(setupMlModuleJobOverridesRT), + datafeedOverrides: rt.array(setupMlModuleDatafeedOverridesRT), }); const setupMlModuleRequestPayloadRT = rt.intersection([ diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/index_patterns_validate.ts b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/validate_indices.ts similarity index 70% rename from x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/index_patterns_validate.ts rename to x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/validate_indices.ts index 440ee10e4223d..0d2e9b673488e 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/index_patterns_validate.ts +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/validate_indices.ts @@ -10,20 +10,22 @@ import { identity } from 'fp-ts/lib/function'; import { kfetch } from 'ui/kfetch'; import { - LOG_ANALYSIS_VALIDATION_INDICES_PATH, + LOG_ANALYSIS_VALIDATE_INDICES_PATH, + ValidationIndicesFieldSpecification, validationIndicesRequestPayloadRT, validationIndicesResponsePayloadRT, } from '../../../../../common/http_api'; import { throwErrors, createPlainError } from '../../../../../common/runtime_types'; -export const callIndexPatternsValidate = async (timestampField: string, indices: string[]) => { +export const callValidateIndicesAPI = async ( + indices: string[], + fields: ValidationIndicesFieldSpecification[] +) => { const response = await kfetch({ method: 'POST', - pathname: LOG_ANALYSIS_VALIDATION_INDICES_PATH, - body: JSON.stringify( - validationIndicesRequestPayloadRT.encode({ data: { timestampField, indices } }) - ), + pathname: LOG_ANALYSIS_VALIDATE_INDICES_PATH, + body: JSON.stringify(validationIndicesRequestPayloadRT.encode({ data: { indices, fields } })), }); return pipe( diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/index.ts b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/index.ts index cbe3b2ef1e9b8..eb044c86e50fe 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/index.ts +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/index.ts @@ -6,7 +6,6 @@ export * from './log_analysis_capabilities'; export * from './log_analysis_cleanup'; -export * from './log_analysis_jobs'; -export * from './log_analysis_results'; -export * from './log_analysis_results_url_state'; -export * from './log_analysis_status_state'; +export * from './log_analysis_module'; +export * from './log_analysis_module_status'; +export * from './log_analysis_module_types'; diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_capabilities.tsx b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_capabilities.tsx index 7ac7d051e6783..35a3ac737ada3 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_capabilities.tsx +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_capabilities.tsx @@ -15,7 +15,7 @@ import { useTrackedPromise } from '../../../utils/use_tracked_promise'; import { getMlCapabilitiesResponsePayloadRT, GetMlCapabilitiesResponsePayload, -} from './ml_api_types'; +} from './api/ml_api_types'; import { throwErrors, createPlainError } from '../../../../common/runtime_types'; export const useLogAnalysisCapabilities = () => { diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_cleanup.tsx b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_cleanup.tsx index 1b79d3c1ef786..a37d18cc33cfd 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_cleanup.tsx +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_cleanup.tsx @@ -4,64 +4,46 @@ * you may not use this file except in compliance with the Elastic License. */ -import createContainer from 'constate'; -import { useMemo } from 'react'; -import { useTrackedPromise } from '../../../utils/use_tracked_promise'; -import { callDeleteJobs, callStopDatafeed, callGetJobDeletionTasks } from './api/ml_cleanup'; -import { getAllModuleJobIds } from '../../../../common/log_analysis'; - -export const useLogAnalysisCleanup = ({ - sourceId, - spaceId, -}: { - sourceId: string; - spaceId: string; -}) => { - const [cleanupMLResourcesRequest, cleanupMLResources] = useTrackedPromise( - { - cancelPreviousOn: 'resolution', - createPromise: async () => { - try { - await callStopDatafeed(spaceId, sourceId); - } catch (err) { - // Datefeed has been deleted / doesn't exist, proceed with deleting jobs anyway - if (err && err.res && err.res.status === 404) { - return await deleteJobs(spaceId, sourceId); - } else { - throw err; - } - } - - return await deleteJobs(spaceId, sourceId); - }, - }, - [spaceId, sourceId] - ); - - const isCleaningUp = useMemo(() => cleanupMLResourcesRequest.state === 'pending', [ - cleanupMLResourcesRequest.state, - ]); +import { getJobId } from '../../../../common/log_analysis'; +import { callDeleteJobs, callGetJobDeletionTasks, callStopDatafeeds } from './api/ml_cleanup'; + +export const cleanUpJobsAndDatafeeds = async ( + spaceId: string, + sourceId: string, + jobTypes: JobType[] +) => { + try { + await callStopDatafeeds(spaceId, sourceId, jobTypes); + } catch (err) { + // Proceed only if datafeed has been deleted or didn't exist in the first place + if (err?.res?.status !== 404) { + throw err; + } + } - return { - cleanupMLResources, - isCleaningUp, - }; + return await deleteJobs(spaceId, sourceId, jobTypes); }; -export const LogAnalysisCleanup = createContainer(useLogAnalysisCleanup); - -const deleteJobs = async (spaceId: string, sourceId: string) => { - const deleteJobsResponse = await callDeleteJobs(spaceId, sourceId); - await waitUntilJobsAreDeleted(spaceId, sourceId); +const deleteJobs = async ( + spaceId: string, + sourceId: string, + jobTypes: JobType[] +) => { + const deleteJobsResponse = await callDeleteJobs(spaceId, sourceId, jobTypes); + await waitUntilJobsAreDeleted(spaceId, sourceId, jobTypes); return deleteJobsResponse; }; -const waitUntilJobsAreDeleted = async (spaceId: string, sourceId: string) => { +const waitUntilJobsAreDeleted = async ( + spaceId: string, + sourceId: string, + jobTypes: JobType[] +) => { + const moduleJobIds = jobTypes.map(jobType => getJobId(spaceId, sourceId, jobType)); while (true) { - const response = await callGetJobDeletionTasks(); - const jobIdsBeingDeleted = response.jobIds; - const moduleJobIds = getAllModuleJobIds(spaceId, sourceId); + const { jobIds: jobIdsBeingDeleted } = await callGetJobDeletionTasks(); const needToWait = jobIdsBeingDeleted.some(jobId => moduleJobIds.includes(jobId)); + if (needToWait) { await timeout(1000); } else { diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_jobs.tsx b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_jobs.tsx deleted file mode 100644 index 0f386f416b866..0000000000000 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_jobs.tsx +++ /dev/null @@ -1,169 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import createContainer from 'constate'; -import { useMemo, useCallback, useEffect } from 'react'; - -import { callGetMlModuleAPI } from './api/ml_get_module'; -import { bucketSpan, getJobId } from '../../../../common/log_analysis'; -import { useTrackedPromise } from '../../../utils/use_tracked_promise'; -import { callJobsSummaryAPI } from './api/ml_get_jobs_summary_api'; -import { callSetupMlModuleAPI, SetupMlModuleResponsePayload } from './api/ml_setup_module_api'; -import { useLogAnalysisCleanup } from './log_analysis_cleanup'; -import { useStatusState } from './log_analysis_status_state'; - -const MODULE_ID = 'logs_ui_analysis'; - -export const useLogAnalysisJobs = ({ - indexPattern, - sourceId, - spaceId, - timeField, -}: { - indexPattern: string; - sourceId: string; - spaceId: string; - timeField: string; -}) => { - const { cleanupMLResources } = useLogAnalysisCleanup({ sourceId, spaceId }); - const [statusState, dispatch] = useStatusState({ - bucketSpan, - indexPattern, - timestampField: timeField, - }); - - const [fetchModuleDefinitionRequest, fetchModuleDefinition] = useTrackedPromise( - { - cancelPreviousOn: 'resolution', - createPromise: async () => { - dispatch({ type: 'fetchingModuleDefinition' }); - return await callGetMlModuleAPI(MODULE_ID); - }, - onResolve: response => { - dispatch({ - type: 'fetchedModuleDefinition', - spaceId, - sourceId, - moduleDefinition: response, - }); - }, - onReject: () => { - dispatch({ type: 'failedFetchingModuleDefinition' }); - }, - }, - [] - ); - - const [setupMlModuleRequest, setupMlModule] = useTrackedPromise( - { - cancelPreviousOn: 'resolution', - createPromise: async ( - indices: string[], - start: number | undefined, - end: number | undefined - ) => { - dispatch({ type: 'startedSetup' }); - return await callSetupMlModuleAPI( - MODULE_ID, - start, - end, - spaceId, - sourceId, - indices.join(','), - timeField, - bucketSpan - ); - }, - onResolve: ({ datafeeds, jobs }: SetupMlModuleResponsePayload) => { - dispatch({ type: 'finishedSetup', datafeeds, jobs, spaceId, sourceId }); - }, - onReject: () => { - dispatch({ type: 'failedSetup' }); - }, - }, - [spaceId, sourceId, timeField, bucketSpan] - ); - - const [fetchJobStatusRequest, fetchJobStatus] = useTrackedPromise( - { - cancelPreviousOn: 'resolution', - createPromise: async () => { - dispatch({ type: 'fetchingJobStatuses' }); - return await callJobsSummaryAPI(spaceId, sourceId); - }, - onResolve: jobResponse => { - dispatch({ type: 'fetchedJobStatuses', payload: jobResponse, spaceId, sourceId }); - }, - onReject: err => { - dispatch({ type: 'failedFetchingJobStatuses' }); - }, - }, - [spaceId, sourceId] - ); - - const isLoadingSetupStatus = useMemo( - () => - fetchJobStatusRequest.state === 'pending' || fetchModuleDefinitionRequest.state === 'pending', - [fetchJobStatusRequest.state, fetchModuleDefinitionRequest.state] - ); - - const availableIndices = useMemo(() => indexPattern.split(','), [indexPattern]); - - const viewResults = useCallback(() => { - dispatch({ type: 'viewedResults' }); - }, []); - - const cleanupAndSetup = useCallback( - (indices: string[], start: number | undefined, end: number | undefined) => { - dispatch({ type: 'startedSetup' }); - cleanupMLResources() - .then(() => { - setupMlModule(indices, start, end); - }) - .catch(() => { - dispatch({ type: 'failedSetup' }); - }); - }, - [cleanupMLResources, setupMlModule] - ); - - const viewSetupForReconfiguration = useCallback(() => { - dispatch({ type: 'requestedJobConfigurationUpdate' }); - }, []); - - const viewSetupForUpdate = useCallback(() => { - dispatch({ type: 'requestedJobDefinitionUpdate' }); - }, []); - - useEffect(() => { - fetchModuleDefinition(); - }, [fetchModuleDefinition]); - - const jobIds = useMemo(() => { - return { - 'log-entry-rate': getJobId(spaceId, sourceId, 'log-entry-rate'), - }; - }, [sourceId, spaceId]); - - return { - availableIndices, - fetchJobStatus, - isLoadingSetupStatus, - jobStatus: statusState.jobStatus, - lastSetupErrorMessages: statusState.lastSetupErrorMessages, - cleanupAndSetup, - setup: setupMlModule, - setupMlModuleRequest, - setupStatus: statusState.setupStatus, - timestampField: timeField, - viewSetupForReconfiguration, - viewSetupForUpdate, - viewResults, - jobIds, - }; -}; - -export const LogAnalysisJobs = createContainer(useLogAnalysisJobs); diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_module.tsx b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_module.tsx new file mode 100644 index 0000000000000..189b58d7923f8 --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_module.tsx @@ -0,0 +1,167 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useCallback, useMemo } from 'react'; + +import { useTrackedPromise } from '../../../utils/use_tracked_promise'; +import { useModuleStatus } from './log_analysis_module_status'; +import { ModuleDescriptor, ModuleSourceConfiguration } from './log_analysis_module_types'; + +export const useLogAnalysisModule = ({ + sourceConfiguration, + moduleDescriptor, +}: { + sourceConfiguration: ModuleSourceConfiguration; + moduleDescriptor: ModuleDescriptor; +}) => { + const { spaceId, sourceId, timestampField, indices } = sourceConfiguration; + const [moduleStatus, dispatchModuleStatus] = useModuleStatus(moduleDescriptor.jobTypes, { + bucketSpan: moduleDescriptor.bucketSpan, + indexPattern: indices.join(','), + timestampField, + }); + + const [fetchModuleDefinitionRequest, fetchModuleDefinition] = useTrackedPromise( + { + cancelPreviousOn: 'resolution', + createPromise: async () => { + dispatchModuleStatus({ type: 'fetchingModuleDefinition' }); + return await moduleDescriptor.getModuleDefinition(); + }, + onResolve: response => { + dispatchModuleStatus({ + type: 'fetchedModuleDefinition', + spaceId, + sourceId, + moduleDefinition: response, + }); + }, + onReject: () => { + dispatchModuleStatus({ type: 'failedFetchingModuleDefinition' }); + }, + }, + [moduleDescriptor.getModuleDefinition, spaceId, sourceId] + ); + + const [fetchJobStatusRequest, fetchJobStatus] = useTrackedPromise( + { + cancelPreviousOn: 'resolution', + createPromise: async () => { + dispatchModuleStatus({ type: 'fetchingJobStatuses' }); + return await moduleDescriptor.getJobSummary(spaceId, sourceId); + }, + onResolve: jobResponse => { + dispatchModuleStatus({ + type: 'fetchedJobStatuses', + payload: jobResponse, + spaceId, + sourceId, + }); + }, + onReject: () => { + dispatchModuleStatus({ type: 'failedFetchingJobStatuses' }); + }, + }, + [spaceId, sourceId] + ); + + const isLoadingModuleStatus = useMemo( + () => + fetchJobStatusRequest.state === 'pending' || fetchModuleDefinitionRequest.state === 'pending', + [fetchJobStatusRequest.state, fetchModuleDefinitionRequest.state] + ); + + const [, setUpModule] = useTrackedPromise( + { + cancelPreviousOn: 'resolution', + createPromise: async ( + selectedIndices: string[], + start: number | undefined, + end: number | undefined + ) => { + dispatchModuleStatus({ type: 'startedSetup' }); + return await moduleDescriptor.setUpModule(start, end, { + indices: selectedIndices, + sourceId, + spaceId, + timestampField, + }); + }, + onResolve: ({ datafeeds, jobs }) => { + dispatchModuleStatus({ type: 'finishedSetup', datafeeds, jobs, spaceId, sourceId }); + }, + onReject: () => { + dispatchModuleStatus({ type: 'failedSetup' }); + }, + }, + [moduleDescriptor.setUpModule, spaceId, sourceId, timestampField] + ); + + const [cleanUpModuleRequest, cleanUpModule] = useTrackedPromise( + { + cancelPreviousOn: 'resolution', + createPromise: async () => { + return await moduleDescriptor.cleanUpModule(spaceId, sourceId); + }, + }, + [spaceId, sourceId] + ); + + const isCleaningUp = useMemo(() => cleanUpModuleRequest.state === 'pending', [ + cleanUpModuleRequest.state, + ]); + + const cleanUpAndSetUpModule = useCallback( + (selectedIndices: string[], start: number | undefined, end: number | undefined) => { + dispatchModuleStatus({ type: 'startedSetup' }); + cleanUpModule() + .then(() => { + setUpModule(selectedIndices, start, end); + }) + .catch(() => { + dispatchModuleStatus({ type: 'failedSetup' }); + }); + }, + [cleanUpModule, setUpModule] + ); + + const viewSetupForReconfiguration = useCallback(() => { + dispatchModuleStatus({ type: 'requestedJobConfigurationUpdate' }); + }, []); + + const viewSetupForUpdate = useCallback(() => { + dispatchModuleStatus({ type: 'requestedJobDefinitionUpdate' }); + }, []); + + const viewResults = useCallback(() => { + dispatchModuleStatus({ type: 'viewedResults' }); + }, []); + + const jobIds = useMemo(() => moduleDescriptor.getJobIds(spaceId, sourceId), [ + moduleDescriptor.getJobIds, + spaceId, + sourceId, + ]); + + return { + cleanUpAndSetUpModule, + cleanUpModule, + fetchJobStatus, + fetchModuleDefinition, + isCleaningUp, + isLoadingModuleStatus, + jobIds, + jobStatus: moduleStatus.jobStatus, + lastSetupErrorMessages: moduleStatus.lastSetupErrorMessages, + moduleDescriptor, + setUpModule, + setupStatus: moduleStatus.setupStatus, + sourceConfiguration, + viewResults, + viewSetupForReconfiguration, + viewSetupForUpdate, + }; +}; diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_status_state.tsx b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_module_status.tsx similarity index 70% rename from x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_status_state.tsx rename to x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_module_status.tsx index 1f4c924ea3da5..6d634538cd7fe 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_status_state.tsx +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_module_status.tsx @@ -7,20 +7,19 @@ import { useReducer } from 'react'; import { + JobSourceConfiguration, + JobStatus, + SetupStatus, getDatafeedId, getJobId, isJobStatusWithResults, - JobStatus, - JobType, - jobTypeRT, - SetupStatus, } from '../../../../common/log_analysis'; import { FetchJobStatusResponsePayload, JobSummary } from './api/ml_get_jobs_summary_api'; import { GetMlModuleResponsePayload, JobDefinition } from './api/ml_get_module'; import { SetupMlModuleResponsePayload } from './api/ml_setup_module_api'; import { MandatoryProperty } from '../../../../common/utility_types'; -interface StatusReducerState { +interface StatusReducerState { jobDefinitions: JobDefinition[]; jobStatus: Record; jobSummaries: JobSummary[]; @@ -65,42 +64,60 @@ type StatusReducerAction = | { type: 'requestedJobDefinitionUpdate' } | { type: 'viewedResults' }; -const createInitialState = (sourceConfiguration: JobSourceConfiguration): StatusReducerState => ({ +const createInitialState = ({ + jobTypes, + sourceConfiguration, +}: { + jobTypes: JobType[]; + sourceConfiguration: JobSourceConfiguration; +}): StatusReducerState => ({ jobDefinitions: [], - jobStatus: { - 'log-entry-rate': 'unknown', - }, + jobStatus: jobTypes.reduce( + (accumulatedJobStatus, jobType) => ({ + ...accumulatedJobStatus, + [jobType]: 'unknown', + }), + {} as Record + ), jobSummaries: [], lastSetupErrorMessages: [], setupStatus: 'initializing', sourceConfiguration, }); -function statusReducer(state: StatusReducerState, action: StatusReducerAction): StatusReducerState { +const createStatusReducer = (jobTypes: JobType[]) => ( + state: StatusReducerState, + action: StatusReducerAction +): StatusReducerState => { switch (action.type) { case 'startedSetup': { return { ...state, - jobStatus: { - 'log-entry-rate': 'initializing', - }, + jobStatus: jobTypes.reduce( + (accumulatedJobStatus, jobType) => ({ + ...accumulatedJobStatus, + [jobType]: 'initializing', + }), + {} as Record + ), setupStatus: 'pending', }; } case 'finishedSetup': { const { jobs, datafeeds, spaceId, sourceId } = action; - const nextJobStatus = { - ...state.jobStatus, - 'log-entry-rate': - hasSuccessfullyCreatedJob(getJobId(spaceId, sourceId, 'log-entry-rate'))(jobs) && - hasSuccessfullyStartedDatafeed(getDatafeedId(spaceId, sourceId, 'log-entry-rate'))( - datafeeds - ) - ? ('started' as JobStatus) - : ('failed' as JobStatus), - }; - const nextSetupStatus = Object.values(nextJobStatus).every(jobState => - ['started'].includes(jobState) + const nextJobStatus = jobTypes.reduce( + (accumulatedJobStatus, jobType) => ({ + ...accumulatedJobStatus, + [jobType]: + hasSuccessfullyCreatedJob(getJobId(spaceId, sourceId, jobType))(jobs) && + hasSuccessfullyStartedDatafeed(getDatafeedId(spaceId, sourceId, jobType))(datafeeds) + ? 'started' + : 'failed', + }), + {} as Record + ); + const nextSetupStatus = Object.values(nextJobStatus).every( + jobState => jobState === 'started' ) ? 'succeeded' : 'failed'; @@ -122,10 +139,13 @@ function statusReducer(state: StatusReducerState, action: StatusReducerAction): case 'failedSetup': { return { ...state, - jobStatus: { - ...state.jobStatus, - 'log-entry-rate': 'failed', - }, + jobStatus: jobTypes.reduce( + (accumulatedJobStatus, jobType) => ({ + ...accumulatedJobStatus, + [jobType]: 'failed', + }), + {} as Record + ), setupStatus: 'failed', }; } @@ -140,10 +160,13 @@ function statusReducer(state: StatusReducerState, action: StatusReducerAction): const { payload: jobSummaries, spaceId, sourceId } = action; const { jobDefinitions, setupStatus, sourceConfiguration } = state; - const nextJobStatus = { - ...state.jobStatus, - 'log-entry-rate': getJobStatus(getJobId(spaceId, sourceId, 'log-entry-rate'))(jobSummaries), - }; + const nextJobStatus = jobTypes.reduce( + (accumulatedJobStatus, jobType) => ({ + ...accumulatedJobStatus, + [jobType]: getJobStatus(getJobId(spaceId, sourceId, jobType))(jobSummaries), + }), + {} as Record + ); const nextSetupStatus = getSetupStatus( spaceId, sourceId, @@ -164,10 +187,13 @@ function statusReducer(state: StatusReducerState, action: StatusReducerAction): return { ...state, setupStatus: 'unknown', - jobStatus: { - ...state.jobStatus, - 'log-entry-rate': 'unknown', - }, + jobStatus: jobTypes.reduce( + (accumulatedJobStatus, jobType) => ({ + ...accumulatedJobStatus, + [jobType]: 'unknown', + }), + {} as Record + ), }; } case 'fetchedModuleDefinition': { @@ -230,7 +256,7 @@ function statusReducer(state: StatusReducerState, action: StatusReducerAction): return state; } } -} +}; const hasSuccessfullyCreatedJob = (jobId: string) => ( jobSetupResponses: SetupMlModuleResponsePayload['jobs'] @@ -281,7 +307,7 @@ const getJobStatus = (jobId: string) => (jobSummaries: FetchJobStatusResponsePay } )[0] || 'missing'; -const getSetupStatus = ( +const getSetupStatus = ( spaceId: string, sourceId: string, sourceConfiguration: JobSourceConfiguration, @@ -289,44 +315,43 @@ const getSetupStatus = ( jobDefinitions: JobDefinition[], jobSummaries: JobSummary[] ) => (previousSetupStatus: SetupStatus) => - Object.entries(everyJobStatus).reduce((setupStatus, [jobType, jobStatus]) => { - if (!jobTypeRT.is(jobType)) { - return setupStatus; - } + Object.entries(everyJobStatus).reduce( + (setupStatus, [jobType, jobStatus]) => { + const jobId = getJobId(spaceId, sourceId, jobType); + const jobDefinition = jobDefinitions.find(({ id }) => id === jobType); - const jobId = getJobId(spaceId, sourceId, jobType); - const jobDefinition = jobDefinitions.find(({ id }) => id === jobType); + if (jobStatus === 'missing') { + return 'required'; + } else if ( + setupStatus === 'required' || + setupStatus === 'requiredForUpdate' || + setupStatus === 'requiredForReconfiguration' + ) { + return setupStatus; + } else if ( + setupStatus === 'skippedButUpdatable' || + (jobDefinition && + !isJobRevisionCurrent( + jobId, + jobDefinition.config.custom_settings.job_revision || 0 + )(jobSummaries)) + ) { + return 'skippedButUpdatable'; + } else if ( + setupStatus === 'skippedButReconfigurable' || + !isJobConfigurationConsistent(jobId, sourceConfiguration)(jobSummaries) + ) { + return 'skippedButReconfigurable'; + } else if (setupStatus === 'hiddenAfterSuccess') { + return setupStatus; + } else if (setupStatus === 'skipped' || isJobStatusWithResults(jobStatus)) { + return 'skipped'; + } - if (jobStatus === 'missing') { - return 'required'; - } else if ( - setupStatus === 'required' || - setupStatus === 'requiredForUpdate' || - setupStatus === 'requiredForReconfiguration' - ) { return setupStatus; - } else if ( - setupStatus === 'skippedButUpdatable' || - (jobDefinition && - !isJobRevisionCurrent( - jobId, - jobDefinition.config.custom_settings.job_revision || 0 - )(jobSummaries)) - ) { - return 'skippedButUpdatable'; - } else if ( - setupStatus === 'skippedButReconfigurable' || - !isJobConfigurationConsistent(jobId, sourceConfiguration)(jobSummaries) - ) { - return 'skippedButReconfigurable'; - } else if (setupStatus === 'hiddenAfterSuccess') { - return setupStatus; - } else if (setupStatus === 'skipped' || isJobStatusWithResults(jobStatus)) { - return 'skipped'; - } - - return setupStatus; - }, previousSetupStatus); + }, + previousSetupStatus + ); const isJobRevisionCurrent = (jobId: string, currentRevision: number) => ( jobSummaries: FetchJobStatusResponsePayload @@ -377,12 +402,13 @@ const isIndexPatternSubset = (indexPatternSubset: string, indexPatternSuperset: const hasError = (value: Value): value is MandatoryProperty => value.error != null; -export const useStatusState = (sourceConfiguration: JobSourceConfiguration) => { - return useReducer(statusReducer, sourceConfiguration, createInitialState); +export const useModuleStatus = ( + jobTypes: JobType[], + sourceConfiguration: JobSourceConfiguration +) => { + return useReducer( + createStatusReducer(jobTypes), + { jobTypes, sourceConfiguration }, + createInitialState + ); }; - -interface JobSourceConfiguration { - bucketSpan: number; - indexPattern: string; - timestampField: string; -} diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_module_types.ts b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_module_types.ts new file mode 100644 index 0000000000000..dc9f25b492635 --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_module_types.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { DeleteJobsResponsePayload } from './api/ml_cleanup'; +import { FetchJobStatusResponsePayload } from './api/ml_get_jobs_summary_api'; +import { GetMlModuleResponsePayload } from './api/ml_get_module'; +import { SetupMlModuleResponsePayload } from './api/ml_setup_module_api'; +import { ValidationIndicesResponsePayload } from '../../../../common/http_api/log_analysis'; + +export interface ModuleDescriptor { + moduleId: string; + jobTypes: JobType[]; + bucketSpan: number; + getJobIds: (spaceId: string, sourceId: string) => Record; + getJobSummary: (spaceId: string, sourceId: string) => Promise; + getModuleDefinition: () => Promise; + setUpModule: ( + start: number | undefined, + end: number | undefined, + sourceConfiguration: ModuleSourceConfiguration + ) => Promise; + cleanUpModule: (spaceId: string, sourceId: string) => Promise; + validateSetupIndices: ( + sourceConfiguration: ModuleSourceConfiguration + ) => Promise; +} + +export interface ModuleSourceConfiguration { + indices: string[]; + sourceId: string; + spaceId: string; + timestampField: string; +} diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_setup_state.tsx b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_setup_state.tsx index c965c50bedccc..275c0194be3b2 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_setup_state.tsx +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_setup_state.tsx @@ -4,15 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useState, useCallback, useMemo, useEffect } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { ValidationIndicesError } from '../../../../common/http_api'; import { isExampleDataIndex } from '../../../../common/log_analysis'; -import { - ValidationIndicesError, - ValidationIndicesResponsePayload, -} from '../../../../common/http_api'; import { useTrackedPromise } from '../../../utils/use_tracked_promise'; -import { callIndexPatternsValidate } from './api/index_patterns_validate'; +import { ModuleDescriptor, ModuleSourceConfiguration } from './log_analysis_module_types'; type SetupHandler = ( indices: string[], @@ -25,53 +22,69 @@ export type ValidationIndicesUIError = | { error: 'NETWORK_ERROR' } | { error: 'TOO_FEW_SELECTED_INDICES' }; -export interface ValidatedIndex { - index: string; - errors: ValidationIndicesError[]; +interface ValidIndex { + validity: 'valid'; + name: string; isSelected: boolean; } -interface AnalysisSetupStateArguments { - availableIndices: string[]; +interface InvalidIndex { + validity: 'invalid'; + name: string; + errors: ValidationIndicesError[]; +} + +export type ValidatedIndex = ValidIndex | InvalidIndex; + +interface AnalysisSetupStateArguments { cleanupAndSetupModule: SetupHandler; + moduleDescriptor: ModuleDescriptor; setupModule: SetupHandler; - timestampField: string; + sourceConfiguration: ModuleSourceConfiguration; } const fourWeeksInMs = 86400000 * 7 * 4; -export const useAnalysisSetupState = ({ - availableIndices, +export const useAnalysisSetupState = ({ cleanupAndSetupModule, + moduleDescriptor: { validateSetupIndices }, setupModule, - timestampField, -}: AnalysisSetupStateArguments) => { + sourceConfiguration, +}: AnalysisSetupStateArguments) => { const [startTime, setStartTime] = useState(Date.now() - fourWeeksInMs); const [endTime, setEndTime] = useState(undefined); - // Prepare the validation - const [validatedIndices, setValidatedIndices] = useState( - availableIndices.map(index => ({ - index, - errors: [], - isSelected: false, - })) - ); + const [validatedIndices, setValidatedIndices] = useState([]); + const [validateIndicesRequest, validateIndices] = useTrackedPromise( { cancelPreviousOn: 'resolution', createPromise: async () => { - return await callIndexPatternsValidate(timestampField, availableIndices); + return await validateSetupIndices(sourceConfiguration); }, - onResolve: ({ data }: ValidationIndicesResponsePayload) => { - setValidatedIndices( - availableIndices.map(index => { - const errors = data.errors.filter(error => error.index === index); - return { - index, - errors, - isSelected: errors.length === 0 && !isExampleDataIndex(index), - }; + onResolve: ({ data: { errors } }) => { + setValidatedIndices(previousValidatedIndices => + sourceConfiguration.indices.map(indexName => { + const previousValidatedIndex = previousValidatedIndices.filter( + ({ name }) => name === indexName + )[0]; + const indexValiationErrors = errors.filter(({ index }) => index === indexName); + if (indexValiationErrors.length > 0) { + return { + validity: 'invalid', + name: indexName, + errors: indexValiationErrors, + }; + } else { + return { + validity: 'valid', + name: indexName, + isSelected: + previousValidatedIndex?.validity === 'valid' + ? previousValidatedIndex?.isSelected + : !isExampleDataIndex(indexName), + }; + } }) ); }, @@ -79,7 +92,7 @@ export const useAnalysisSetupState = ({ setValidatedIndices([]); }, }, - [availableIndices, timestampField] + [sourceConfiguration.indices] ); useEffect(() => { @@ -87,7 +100,10 @@ export const useAnalysisSetupState = ({ }, [validateIndices]); const selectedIndexNames = useMemo( - () => validatedIndices.filter(i => i.isSelected).map(i => i.index), + () => + validatedIndices + .filter(index => index.validity === 'valid' && index.isSelected) + .map(i => i.name), [validatedIndices] ); @@ -120,7 +136,9 @@ export const useAnalysisSetupState = ({ } return validatedIndices.reduce((errors, index) => { - return selectedIndexNames.includes(index.index) ? errors.concat(index.errors) : errors; + return index.validity === 'invalid' && selectedIndexNames.includes(index.name) + ? [...errors, ...index.errors] + : errors; }, []); }, [selectedIndexNames, validatedIndices, validateIndicesRequest.state]); diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_entry_rate.tsx b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_entry_rate.tsx deleted file mode 100644 index 8b21a7e829894..0000000000000 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_entry_rate.tsx +++ /dev/null @@ -1,50 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { useMemo, useState } from 'react'; - -import { GetLogEntryRateSuccessResponsePayload } from '../../../../common/http_api/log_analysis'; -import { useTrackedPromise } from '../../../utils/use_tracked_promise'; -import { callGetLogEntryRateAPI } from './api/get_log_entry_rate'; - -type LogEntryRateResults = GetLogEntryRateSuccessResponsePayload['data']; - -export const useLogEntryRate = ({ - sourceId, - startTime, - endTime, - bucketDuration, -}: { - sourceId: string; - startTime: number; - endTime: number; - bucketDuration: number; -}) => { - const [logEntryRate, setLogEntryRate] = useState(null); - - const [getLogEntryRateRequest, getLogEntryRate] = useTrackedPromise( - { - cancelPreviousOn: 'resolution', - createPromise: async () => { - return await callGetLogEntryRateAPI(sourceId, startTime, endTime, bucketDuration); - }, - onResolve: response => { - setLogEntryRate(response.data); - }, - }, - [sourceId, startTime, endTime, bucketDuration] - ); - - const isLoading = useMemo(() => getLogEntryRateRequest.state === 'pending', [ - getLogEntryRateRequest.state, - ]); - - return { - getLogEntryRate, - isLoading, - logEntryRate, - }; -}; diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/ml_api_types.ts b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/ml_api_types.ts deleted file mode 100644 index ee70edc31d49b..0000000000000 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/ml_api_types.ts +++ /dev/null @@ -1,28 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import * as rt from 'io-ts'; - -export const getMlCapabilitiesResponsePayloadRT = rt.type({ - capabilities: rt.type({ - canGetJobs: rt.boolean, - canCreateJob: rt.boolean, - canDeleteJob: rt.boolean, - canOpenJob: rt.boolean, - canCloseJob: rt.boolean, - canForecastJob: rt.boolean, - canGetDatafeeds: rt.boolean, - canStartStopDatafeed: rt.boolean, - canUpdateJob: rt.boolean, - canUpdateDatafeed: rt.boolean, - canPreviewDatafeed: rt.boolean, - }), - isPlatinumOrTrialLicense: rt.boolean, - mlFeatureEnabledInSpace: rt.boolean, - upgradeInProgress: rt.boolean, -}); - -export type GetMlCapabilitiesResponsePayload = rt.TypeOf; diff --git a/x-pack/legacy/plugins/infra/public/containers/source/index.ts b/x-pack/legacy/plugins/infra/public/containers/source/index.ts index 9442836f2a6c6..5911decf21774 100644 --- a/x-pack/legacy/plugins/infra/public/containers/source/index.ts +++ b/x-pack/legacy/plugins/infra/public/containers/source/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { Source, useSource } from './source'; +export * from './source'; diff --git a/x-pack/legacy/plugins/infra/public/containers/source/source.tsx b/x-pack/legacy/plugins/infra/public/containers/source/source.tsx index 955529c9759c4..4729f7aa31f0b 100644 --- a/x-pack/legacy/plugins/infra/public/containers/source/source.tsx +++ b/x-pack/legacy/plugins/infra/public/containers/source/source.tsx @@ -176,3 +176,4 @@ export const useSource = ({ sourceId }: { sourceId: string }) => { }; export const Source = createContainer(useSource); +export const [SourceProvider, useSourceContext] = Source; diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/index.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/index.tsx index 1630de11bbdff..4eddecf732f75 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/index.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/index.tsx @@ -21,7 +21,7 @@ import { Source, useSource } from '../../containers/source'; import { StreamPage } from './stream'; import { SettingsPage } from '../shared/settings'; import { AppNavigation } from '../../components/navigation/app_navigation'; -import { AnalysisPage } from './analysis'; +import { LogEntryRatePage } from './log_entry_rate'; import { useLogAnalysisCapabilities, LogAnalysisCapabilities, @@ -98,7 +98,7 @@ export const LogsPage = injectUICapabilities(({ match, uiCapabilities }: LogsPag - + + jobTypes.reduce( + (accumulatedJobIds, jobType) => ({ + ...accumulatedJobIds, + [jobType]: getJobId(spaceId, sourceId, jobType), + }), + {} as Record + ); + +const getJobSummary = async (spaceId: string, sourceId: string) => { + const response = await callJobsSummaryAPI(spaceId, sourceId, jobTypes); + const jobIds = Object.values(getJobIds(spaceId, sourceId)); + + return response.filter(jobSummary => jobIds.includes(jobSummary.id)); +}; + +const getModuleDefinition = async () => { + return await callGetMlModuleAPI(moduleId); +}; + +const setUpModule = async ( + start: number | undefined, + end: number | undefined, + { spaceId, sourceId, indices, timestampField }: ModuleSourceConfiguration +) => { + const indexNamePattern = indices.join(','); + const jobOverrides = [ + { + job_id: 'log-entry-rate' as const, + analysis_config: { + bucket_span: `${bucketSpan}ms`, + }, + data_description: { + time_field: timestampField, + }, + custom_settings: { + logs_source_config: { + indexPattern: indexNamePattern, + timestampField, + bucketSpan, + }, + }, + }, + ]; + + return callSetupMlModuleAPI( + moduleId, + start, + end, + spaceId, + sourceId, + indexNamePattern, + jobOverrides + ); +}; + +const cleanUpModule = async (spaceId: string, sourceId: string) => { + return await cleanUpJobsAndDatafeeds(spaceId, sourceId, jobTypes); +}; + +const validateSetupIndices = async ({ indices, timestampField }: ModuleSourceConfiguration) => { + return await callValidateIndicesAPI(indices, [ + { + name: timestampField, + validTypes: ['date'], + }, + { + name: partitionField, + validTypes: ['keyword'], + }, + ]); +}; + +export const logEntryRateModule: ModuleDescriptor = { + moduleId, + jobTypes, + bucketSpan, + getJobIds, + getJobSummary, + getModuleDefinition, + setUpModule, + cleanUpModule, + validateSetupIndices, +}; diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/page.tsx similarity index 53% rename from x-pack/legacy/plugins/infra/public/pages/logs/analysis/page.tsx rename to x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/page.tsx index d82da895f9a5a..5ff5cd4db7168 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/page.tsx @@ -7,15 +7,15 @@ import React from 'react'; import { ColumnarPage } from '../../../components/page'; -import { AnalysisPageContent } from './page_content'; -import { AnalysisPageProviders } from './page_providers'; +import { LogEntryRatePageContent } from './page_content'; +import { LogEntryRatePageProviders } from './page_providers'; -export const AnalysisPage = () => { +export const LogEntryRatePage = () => { return ( - - - + + + - + ); }; diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_content.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/page_content.tsx similarity index 59% rename from x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_content.tsx rename to x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/page_content.tsx index f0a26eae25ecb..e62164cb17b2c 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_content.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/page_content.tsx @@ -9,34 +9,37 @@ import React, { useContext, useEffect } from 'react'; import { isSetupStatusWithResults } from '../../../../common/log_analysis'; import { LoadingPage } from '../../../components/loading_page'; -import { LogAnalysisCapabilities, LogAnalysisJobs } from '../../../containers/logs/log_analysis'; +import { LogAnalysisCapabilities } from '../../../containers/logs/log_analysis'; import { Source } from '../../../containers/source'; -import { AnalysisResultsContent } from './page_results_content'; -import { AnalysisSetupContent } from './page_setup_content'; -import { AnalysisUnavailableContent } from './page_unavailable_content'; -import { AnalysisSetupStatusUnknownContent } from './page_setup_status_unknown'; +import { LogEntryRateResultsContent } from './page_results_content'; +import { LogEntryRateSetupContent } from './page_setup_content'; +import { LogEntryRateUnavailableContent } from './page_unavailable_content'; +import { LogEntryRateSetupStatusUnknownContent } from './page_setup_status_unknown'; +import { useLogEntryRateModuleContext } from './use_log_entry_rate_module'; -export const AnalysisPageContent = () => { +export const LogEntryRatePageContent = () => { const { sourceId } = useContext(Source.Context); const { hasLogAnalysisCapabilites } = useContext(LogAnalysisCapabilities.Context); const { - availableIndices, - cleanupAndSetup, + cleanUpAndSetUpModule: cleanupAndSetup, fetchJobStatus, + fetchModuleDefinition, lastSetupErrorMessages, - setup, + moduleDescriptor, + setUpModule, setupStatus, - timestampField, + sourceConfiguration, viewResults, - } = useContext(LogAnalysisJobs.Context); + } = useLogEntryRateModuleContext(); useEffect(() => { + fetchModuleDefinition(); fetchJobStatus(); }, []); if (!hasLogAnalysisCapabilites) { - return ; + return ; } else if (setupStatus === 'initializing') { return ( { /> ); } else if (setupStatus === 'unknown') { - return ; + return ; } else if (isSetupStatusWithResults(setupStatus)) { return ( - ); } else { return ( - ); diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_providers.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/page_providers.tsx similarity index 53% rename from x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_providers.tsx rename to x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/page_providers.tsx index fba32f6cbd6d0..67c8ea7660a26 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_providers.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/page_providers.tsx @@ -4,24 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useContext } from 'react'; +import React from 'react'; -import { LogAnalysisJobs } from '../../../containers/logs/log_analysis'; -import { Source } from '../../../containers/source'; +import { useSourceContext } from '../../../containers/source'; import { useKibanaSpaceId } from '../../../utils/use_kibana_space_id'; +import { LogEntryRateModuleProvider } from './use_log_entry_rate_module'; -export const AnalysisPageProviders: React.FunctionComponent = ({ children }) => { - const { sourceId, source } = useContext(Source.Context); +export const LogEntryRatePageProviders: React.FunctionComponent = ({ children }) => { + const { sourceId, source } = useSourceContext(); const spaceId = useKibanaSpaceId(); return ( - {children} - + ); }; diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_results_content.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx similarity index 78% rename from x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_results_content.tsx rename to x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx index 7fa9ff3c93db7..be637bc29a0db 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_results_content.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx @@ -17,36 +17,36 @@ import { import numeral from '@elastic/numeral'; import { FormattedMessage } from '@kbn/i18n/react'; import moment from 'moment'; -import React, { useCallback, useContext, useMemo, useState } from 'react'; +import React, { useCallback, useMemo, useState, useEffect } from 'react'; import euiStyled from '../../../../../../common/eui_styled_components'; import { TimeRange } from '../../../../common/http_api/shared/time_range'; import { bucketSpan } from '../../../../common/log_analysis'; import { LoadingOverlayWrapper } from '../../../components/loading_overlay_wrapper'; -import { - LogAnalysisJobs, - StringTimeRange, - useLogAnalysisResults, - useLogAnalysisResultsUrlState, -} from '../../../containers/logs/log_analysis'; import { useInterval } from '../../../hooks/use_interval'; import { useTrackPageview } from '../../../hooks/use_track_metric'; import { useKibanaUiSetting } from '../../../utils/use_kibana_ui_setting'; import { FirstUseCallout } from './first_use'; import { AnomaliesResults } from './sections/anomalies'; import { LogRateResults } from './sections/log_rate'; +import { useLogEntryRateModuleContext } from './use_log_entry_rate_module'; +import { useLogEntryRateResults } from './use_log_entry_rate_results'; +import { + StringTimeRange, + useLogAnalysisResultsUrlState, +} from './use_log_entry_rate_results_url_state'; const JOB_STATUS_POLLING_INTERVAL = 30000; -export const AnalysisResultsContent = ({ +export const LogEntryRateResultsContent = ({ sourceId, isFirstUse, }: { sourceId: string; isFirstUse: boolean; }) => { - useTrackPageview({ app: 'infra_logs', path: 'analysis_results' }); - useTrackPageview({ app: 'infra_logs', path: 'analysis_results', delay: 15000 }); + useTrackPageview({ app: 'infra_logs', path: 'log_entry_rate_results' }); + useTrackPageview({ app: 'infra_logs', path: 'log_entry_rate_results', delay: 15000 }); const [dateFormat] = useKibanaUiSetting('dateFormat', 'MMMM D, YYYY h:mm A'); @@ -65,32 +65,20 @@ export const AnalysisResultsContent = ({ lastChangedTime: Date.now(), })); - const bucketDuration = useMemo(() => { - // This function takes the current time range in ms, - // works out the bucket interval we'd need to always - // display 100 data points, and then takes that new - // value and works out the nearest multiple of - // 900000 (15 minutes) to it, so that we don't end up with - // jaggy bucket boundaries between the ML buckets and our - // aggregation buckets. - const msRange = moment(queryTimeRange.value.endTime).diff( - moment(queryTimeRange.value.startTime) - ); - const bucketIntervalInMs = msRange / 100; - const result = bucketSpan * Math.round(bucketIntervalInMs / bucketSpan); - const roundedResult = parseInt(Number(result).toFixed(0), 10); - return roundedResult < bucketSpan ? bucketSpan : roundedResult; - }, [queryTimeRange.value.startTime, queryTimeRange.value.endTime]); + const bucketDuration = useMemo( + () => getBucketDuration(queryTimeRange.value.startTime, queryTimeRange.value.endTime), + [queryTimeRange.value.endTime, queryTimeRange.value.startTime] + ); - const { isLoading, logRateResults } = useLogAnalysisResults({ + const { getLogEntryRate, isLoading, logEntryRate } = useLogEntryRateResults({ sourceId, startTime: queryTimeRange.value.startTime, endTime: queryTimeRange.value.endTime, bucketDuration, - lastRequestTime: queryTimeRange.lastChangedTime, }); - const hasResults = useMemo(() => logRateResults && logRateResults.histogramBuckets.length > 0, [ - logRateResults, + + const hasResults = useMemo(() => (logEntryRate?.histogramBuckets?.length ?? 0) > 0, [ + logEntryRate, ]); const handleQueryTimeRangeChange = useCallback( @@ -145,7 +133,11 @@ export const AnalysisResultsContent = ({ viewSetupForReconfiguration, viewSetupForUpdate, jobIds, - } = useContext(LogAnalysisJobs.Context); + } = useLogEntryRateModuleContext(); + + useEffect(() => { + getLogEntryRate(); + }, [getLogEntryRate, queryTimeRange.lastChangedTime]); useInterval(() => { fetchJobStatus(); @@ -168,7 +160,7 @@ export const AnalysisResultsContent = ({ - {logRateResults ? ( + {logEntryRate ? ( - {numeral(logRateResults.totalNumberOfLogEntries).format('0.00a')} + {numeral(logEntryRate.totalNumberOfLogEntries).format('0.00a')} ), @@ -210,7 +202,7 @@ export const AnalysisResultsContent = ({ {isFirstUse && !hasResults ? : null} @@ -223,7 +215,7 @@ export const AnalysisResultsContent = ({ jobStatus={jobStatus['log-entry-rate']} viewSetupForReconfiguration={viewSetupForReconfiguration} viewSetupForUpdate={viewSetupForUpdate} - results={logRateResults} + results={logEntryRate} setTimeRange={handleChartTimeRangeChange} setupStatus={setupStatus} timeRange={queryTimeRange.value} @@ -250,6 +242,23 @@ const stringToNumericTimeRange = (timeRange: StringTimeRange): TimeRange => ({ ).valueOf(), }); +/** + * This function takes the current time range in ms, + * works out the bucket interval we'd need to always + * display 100 data points, and then takes that new + * value and works out the nearest multiple of + * 900000 (15 minutes) to it, so that we don't end up with + * jaggy bucket boundaries between the ML buckets and our + * aggregation buckets. + */ +const getBucketDuration = (startTime: number, endTime: number) => { + const msRange = moment(endTime).diff(moment(startTime)); + const bucketIntervalInMs = msRange / 100; + const result = bucketSpan * Math.round(bucketIntervalInMs / bucketSpan); + const roundedResult = parseInt(Number(result).toFixed(0), 10); + return roundedResult < bucketSpan ? bucketSpan : roundedResult; +}; + // This is needed due to the flex-basis: 100% !important; rule that // kicks in on small screens via media queries breaking when using direction="column" export const ResultsContentPage = euiStyled(EuiPage)` diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_setup_content.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/page_setup_content.tsx similarity index 70% rename from x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_setup_content.tsx rename to x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/page_setup_content.tsx index 7ae174c4a7899..6c04404b91231 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_setup_content.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/page_setup_content.tsx @@ -4,23 +4,25 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; import { EuiPage, EuiPageBody, EuiPageContent, + EuiPageContentBody, EuiPageContentHeader, EuiPageContentHeaderSection, - EuiPageContentBody, + EuiSpacer, EuiText, EuiTitle, - EuiSpacer, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import React from 'react'; + import euiStyled from '../../../../../../common/eui_styled_components'; import { SetupStatus } from '../../../../common/log_analysis'; +import { ModuleDescriptor, ModuleSourceConfiguration } from '../../../containers/logs/log_analysis'; import { useTrackPageview } from '../../../hooks/use_track_metric'; -import { AnalysisSetupSteps } from './setup'; +import { LogEntryRateSetupSteps } from './setup'; type SetupHandler = ( indices: string[], @@ -28,32 +30,32 @@ type SetupHandler = ( endTime: number | undefined ) => void; -interface AnalysisSetupContentProps { - availableIndices: string[]; +interface LogEntryRateSetupContentProps { cleanupAndSetup: SetupHandler; errorMessages: string[]; + moduleDescriptor: ModuleDescriptor; setup: SetupHandler; setupStatus: SetupStatus; - timestampField: string; + sourceConfiguration: ModuleSourceConfiguration; viewResults: () => void; } -export const AnalysisSetupContent: React.FunctionComponent = ({ - availableIndices, +export const LogEntryRateSetupContent = ({ cleanupAndSetup, errorMessages, setup, setupStatus, - timestampField, viewResults, -}) => { - useTrackPageview({ app: 'infra_logs', path: 'analysis_setup' }); - useTrackPageview({ app: 'infra_logs', path: 'analysis_setup', delay: 15000 }); + moduleDescriptor, + sourceConfiguration, +}: LogEntryRateSetupContentProps) => { + useTrackPageview({ app: 'infra_logs', path: 'log_entry_rate_setup' }); + useTrackPageview({ app: 'infra_logs', path: 'log_entry_rate_setup', delay: 15000 }); return ( - + - - - + - + ); }; // !important due to https://github.com/elastic/eui/issues/2232 -const AnalysisPageContent = euiStyled(EuiPageContent)` +const LogEntryRateSetupPageContent = euiStyled(EuiPageContent)` max-width: 768px !important; `; -const AnalysisSetupPage = euiStyled(EuiPage)` +const LogEntryRateSetupPage = euiStyled(EuiPage)` height: 100%; `; diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_setup_status_unknown.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/page_setup_status_unknown.tsx similarity index 92% rename from x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_setup_status_unknown.tsx rename to x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/page_setup_status_unknown.tsx index 953b0841ffe92..4c685bd42b937 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_setup_status_unknown.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/page_setup_status_unknown.tsx @@ -14,7 +14,7 @@ interface Props { retry: () => void; } -export const AnalysisSetupStatusUnknownContent: React.FunctionComponent = ({ +export const LogEntryRateSetupStatusUnknownContent: React.FunctionComponent = ({ retry, }: Props) => ( = () => ( +export const LogEntryRateUnavailableContent: React.FunctionComponent<{}> = () => ( diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/anomalies/chart.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/chart.tsx similarity index 100% rename from x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/anomalies/chart.tsx rename to x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/chart.tsx diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/anomalies/expanded_row.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/expanded_row.tsx similarity index 84% rename from x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/anomalies/expanded_row.tsx rename to x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/expanded_row.tsx index 0586f5282ddf7..f8a7f12364cf9 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/anomalies/expanded_row.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/expanded_row.tsx @@ -4,38 +4,37 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useMemo } from 'react'; -import { i18n } from '@kbn/i18n'; +import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiStat } from '@elastic/eui'; import numeral from '@elastic/numeral'; -import { EuiFlexGroup, EuiFlexItem, EuiStat, EuiSpacer } from '@elastic/eui'; -import { AnomaliesChart } from './chart'; -import { LogRateResults } from '../../../../../containers/logs/log_analysis/log_analysis_results'; +import { i18n } from '@kbn/i18n'; +import React, { useMemo } from 'react'; + import { TimeRange } from '../../../../../../common/http_api/shared/time_range'; +import { AnalyzeInMlButton } from '../../../../../components/logging/log_analysis_results'; +import { LogEntryRateResults } from '../../use_log_entry_rate_results'; import { - getLogEntryRateSeriesForPartition, getAnnotationsForPartition, + getLogEntryRateSeriesForPartition, getTotalNumberOfLogEntriesForPartition, } from '../helpers/data_formatters'; -import { AnalyzeInMlButton } from '../analyze_in_ml_button'; +import { AnomaliesChart } from './chart'; export const AnomaliesTableExpandedRow: React.FunctionComponent<{ partitionId: string; topAnomalyScore: number; - results: LogRateResults; + results: LogEntryRateResults; setTimeRange: (timeRange: TimeRange) => void; timeRange: TimeRange; jobId: string; }> = ({ results, timeRange, setTimeRange, partitionId, jobId }) => { const logEntryRateSeries = useMemo( () => - results && results.histogramBuckets - ? getLogEntryRateSeriesForPartition(results, partitionId) - : [], + results?.histogramBuckets ? getLogEntryRateSeriesForPartition(results, partitionId) : [], [results, partitionId] ); const anomalyAnnotations = useMemo( () => - results && results.histogramBuckets + results?.histogramBuckets ? getAnnotationsForPartition(results, partitionId) : { warning: [], @@ -47,7 +46,7 @@ export const AnomaliesTableExpandedRow: React.FunctionComponent<{ ); const totalNumberOfLogEntries = useMemo( () => - results && results.histogramBuckets + results?.histogramBuckets ? getTotalNumberOfLogEntriesForPartition(results, partitionId) : undefined, [results, partitionId] diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/anomalies/index.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/index.tsx similarity index 97% rename from x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/anomalies/index.tsx rename to x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/index.tsx index e870c2d442719..38aa4b068c9e9 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/anomalies/index.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/index.tsx @@ -18,7 +18,7 @@ import { i18n } from '@kbn/i18n'; import React, { useMemo } from 'react'; import euiStyled from '../../../../../../../../common/eui_styled_components'; -import { LogRateResults } from '../../../../../containers/logs/log_analysis/log_analysis_results'; +import { LogEntryRateResults } from '../../use_log_entry_rate_results'; import { TimeRange } from '../../../../../../common/http_api/shared/time_range'; import { JobStatus, SetupStatus } from '../../../../../../common/log_analysis'; import { @@ -30,13 +30,13 @@ import { import { AnomaliesChart } from './chart'; import { AnomaliesTable } from './table'; import { LogAnalysisJobProblemIndicator } from '../../../../../components/logging/log_analysis_job_status'; -import { AnalyzeInMlButton } from '../analyze_in_ml_button'; +import { AnalyzeInMlButton } from '../../../../../components/logging/log_analysis_results'; import { LoadingOverlayWrapper } from '../../../../../components/loading_overlay_wrapper'; export const AnomaliesResults: React.FunctionComponent<{ isLoading: boolean; jobStatus: JobStatus; - results: LogRateResults | null; + results: LogEntryRateResults | null; setTimeRange: (timeRange: TimeRange) => void; setupStatus: SetupStatus; timeRange: TimeRange; diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/anomalies/table.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/table.tsx similarity index 97% rename from x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/anomalies/table.tsx rename to x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/table.tsx index c0016d07c290b..2057d75f72354 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/anomalies/table.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/table.tsx @@ -9,7 +9,7 @@ import { EuiBasicTable, EuiButtonIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { RIGHT_ALIGNMENT } from '@elastic/eui/lib/services'; import { TimeRange } from '../../../../../../common/http_api/shared/time_range'; -import { LogRateResults } from '../../../../../containers/logs/log_analysis/log_analysis_results'; +import { LogEntryRateResults } from '../../use_log_entry_rate_results'; import { AnomaliesTableExpandedRow } from './expanded_row'; import { formatAnomalyScore, getFriendlyNameForPartitionId } from '../helpers/data_formatters'; import euiStyled from '../../../../../../../../common/eui_styled_components'; @@ -50,7 +50,7 @@ const maxAnomalyScoreColumnName = i18n.translate( ); export const AnomaliesTable: React.FunctionComponent<{ - results: LogRateResults; + results: LogEntryRateResults; setTimeRange: (timeRange: TimeRange) => void; timeRange: TimeRange; jobId: string; diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/helpers/data_formatters.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/sections/helpers/data_formatters.tsx similarity index 88% rename from x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/helpers/data_formatters.tsx rename to x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/sections/helpers/data_formatters.tsx index 74a3b5f80a577..f9b85fc4e20c2 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/helpers/data_formatters.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/sections/helpers/data_formatters.tsx @@ -6,18 +6,19 @@ import { RectAnnotationDatum } from '@elastic/charts'; import { i18n } from '@kbn/i18n'; -import { LogRateResults } from '../../../../../containers/logs/log_analysis/log_analysis_results'; -export type MLSeverityScoreCategories = 'warning' | 'minor' | 'major' | 'critical'; -type MLSeverityScores = Record; -const ML_SEVERITY_SCORES: MLSeverityScores = { +import { LogEntryRateResults } from '../../use_log_entry_rate_results'; + +const ML_SEVERITY_SCORES = { warning: 3, minor: 25, major: 50, critical: 75, }; -export const getLogEntryRatePartitionedSeries = (results: LogRateResults) => { +export type MLSeverityScoreCategories = keyof typeof ML_SEVERITY_SCORES; + +export const getLogEntryRatePartitionedSeries = (results: LogEntryRateResults) => { return results.histogramBuckets.reduce>( (buckets, bucket) => { return [ @@ -33,7 +34,7 @@ export const getLogEntryRatePartitionedSeries = (results: LogRateResults) => { ); }; -export const getLogEntryRateCombinedSeries = (results: LogRateResults) => { +export const getLogEntryRateCombinedSeries = (results: LogEntryRateResults) => { return results.histogramBuckets.reduce>( (buckets, bucket) => { return [ @@ -50,7 +51,10 @@ export const getLogEntryRateCombinedSeries = (results: LogRateResults) => { ); }; -export const getLogEntryRateSeriesForPartition = (results: LogRateResults, partitionId: string) => { +export const getLogEntryRateSeriesForPartition = ( + results: LogEntryRateResults, + partitionId: string +) => { return results.partitionBuckets[partitionId].buckets.reduce< Array<{ time: number; value: number }> >((buckets, bucket) => { @@ -64,7 +68,7 @@ export const getLogEntryRateSeriesForPartition = (results: LogRateResults, parti }, []); }; -export const getAnnotationsForPartition = (results: LogRateResults, partitionId: string) => { +export const getAnnotationsForPartition = (results: LogEntryRateResults, partitionId: string) => { return results.partitionBuckets[partitionId].buckets.reduce< Record >( @@ -106,13 +110,13 @@ export const getAnnotationsForPartition = (results: LogRateResults, partitionId: }; export const getTotalNumberOfLogEntriesForPartition = ( - results: LogRateResults, + results: LogEntryRateResults, partitionId: string ) => { return results.partitionBuckets[partitionId].totalNumberOfLogEntries; }; -export const getAnnotationsForAll = (results: LogRateResults) => { +export const getAnnotationsForAll = (results: LogEntryRateResults) => { return results.histogramBuckets.reduce>( (annotatedBucketsBySeverity, bucket) => { const maxAnomalyScoresByPartition = bucket.partitions.reduce< @@ -169,7 +173,7 @@ export const getAnnotationsForAll = (results: LogRateResults) => { ); }; -export const getTopAnomalyScoreAcrossAllPartitions = (results: LogRateResults) => { +export const getTopAnomalyScoreAcrossAllPartitions = (results: LogEntryRateResults) => { const allTopScores = Object.values(results.partitionBuckets).reduce( (scores: number[], partition) => { return [...scores, partition.topAnomalyScore]; diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/log_rate/bar_chart.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/sections/log_rate/bar_chart.tsx similarity index 100% rename from x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/log_rate/bar_chart.tsx rename to x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/sections/log_rate/bar_chart.tsx diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/log_rate/index.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/sections/log_rate/index.tsx similarity index 96% rename from x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/log_rate/index.tsx rename to x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/sections/log_rate/index.tsx index 44805520f3b9e..a11dc9d4d607a 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/log_rate/index.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/sections/log_rate/index.tsx @@ -8,7 +8,7 @@ import { EuiEmptyPrompt, EuiLoadingSpinner, EuiSpacer, EuiTitle, EuiText } from import { i18n } from '@kbn/i18n'; import React, { useMemo } from 'react'; -import { LogRateResults as Results } from '../../../../../containers/logs/log_analysis/log_analysis_results'; +import { LogEntryRateResults as Results } from '../../use_log_entry_rate_results'; import { TimeRange } from '../../../../../../common/http_api/shared/time_range'; import { LogEntryRateBarChart } from './bar_chart'; import { getLogEntryRatePartitionedSeries } from '../helpers/data_formatters'; diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/get_log_entry_rate.ts b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/service_calls/get_log_entry_rate.ts similarity index 100% rename from x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/get_log_entry_rate.ts rename to x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/service_calls/get_log_entry_rate.ts diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/index.ts b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/setup/index.ts similarity index 100% rename from x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/index.ts rename to x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/setup/index.ts diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/initial_configuration_step/analysis_setup_indices_form.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/setup/initial_configuration_step/analysis_setup_indices_form.tsx similarity index 91% rename from x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/initial_configuration_step/analysis_setup_indices_form.tsx rename to x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/setup/initial_configuration_step/analysis_setup_indices_form.tsx index 585a65b9ad1c8..91662c49adace 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/initial_configuration_step/analysis_setup_indices_form.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/setup/initial_configuration_step/analysis_setup_indices_form.tsx @@ -25,7 +25,7 @@ export const AnalysisSetupIndicesForm: React.FunctionComponent<{ onChangeSelectedIndices( indices.map(index => { const checkbox = event.currentTarget; - return index.index === checkbox.id ? { ...index, isSelected: checkbox.checked } : index; + return index.name === checkbox.id ? { ...index, isSelected: checkbox.checked } : index; }) ); }, @@ -35,22 +35,21 @@ export const AnalysisSetupIndicesForm: React.FunctionComponent<{ const choices = useMemo( () => indices.map(index => { - const validIndex = index.errors.length === 0; const checkbox = ( {index.index}} + key={index.name} + id={index.name} + label={{index.name}} onChange={handleCheckboxChange} - checked={index.isSelected} - disabled={!validIndex} + checked={index.validity === 'valid' && index.isSelected} + disabled={index.validity === 'invalid'} /> ); - return validIndex ? ( + return index.validity === 'valid' ? ( checkbox ) : ( -
+
{checkbox}
); diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/initial_configuration_step/analysis_setup_timerange_form.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/setup/initial_configuration_step/analysis_setup_timerange_form.tsx similarity index 100% rename from x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/initial_configuration_step/analysis_setup_timerange_form.tsx rename to x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/setup/initial_configuration_step/analysis_setup_timerange_form.tsx diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/initial_configuration_step/index.ts b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/setup/initial_configuration_step/index.ts similarity index 100% rename from x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/initial_configuration_step/index.ts rename to x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/setup/initial_configuration_step/index.ts diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/initial_configuration_step/initial_configuration_step.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/setup/initial_configuration_step/initial_configuration_step.tsx similarity index 100% rename from x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/initial_configuration_step/initial_configuration_step.tsx rename to x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/setup/initial_configuration_step/initial_configuration_step.tsx diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/process_step/create_ml_jobs_button.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/setup/process_step/create_ml_jobs_button.tsx similarity index 100% rename from x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/process_step/create_ml_jobs_button.tsx rename to x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/setup/process_step/create_ml_jobs_button.tsx diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/process_step/index.ts b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/setup/process_step/index.ts similarity index 100% rename from x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/process_step/index.ts rename to x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/setup/process_step/index.ts diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/process_step/process_step.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/setup/process_step/process_step.tsx similarity index 100% rename from x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/process_step/process_step.tsx rename to x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/setup/process_step/process_step.tsx diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/process_step/recreate_ml_jobs_button.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/setup/process_step/recreate_ml_jobs_button.tsx similarity index 100% rename from x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/process_step/recreate_ml_jobs_button.tsx rename to x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/setup/process_step/recreate_ml_jobs_button.tsx diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/setup_steps.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/setup/setup_steps.tsx similarity index 84% rename from x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/setup_steps.tsx rename to x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/setup/setup_steps.tsx index 4643516e73fac..967c69dfae950 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/setup_steps.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/setup/setup_steps.tsx @@ -12,6 +12,10 @@ import { SetupStatus } from '../../../../../common/log_analysis'; import { useAnalysisSetupState } from '../../../../containers/logs/log_analysis/log_analysis_setup_state'; import { InitialConfigurationStep } from './initial_configuration_step'; import { ProcessStep } from './process_step'; +import { + ModuleDescriptor, + ModuleSourceConfiguration, +} from '../../../../containers/logs/log_analysis'; type SetupHandler = ( indices: string[], @@ -19,25 +23,25 @@ type SetupHandler = ( endTime: number | undefined ) => void; -interface AnalysisSetupStepsProps { - availableIndices: string[]; +interface LogEntryRateSetupStepsProps { cleanupAndSetup: SetupHandler; errorMessages: string[]; setup: SetupHandler; setupStatus: SetupStatus; - timestampField: string; viewResults: () => void; + moduleDescriptor: ModuleDescriptor; + sourceConfiguration: ModuleSourceConfiguration; } -export const AnalysisSetupSteps: React.FunctionComponent = ({ - availableIndices, +export const LogEntryRateSetupSteps = ({ cleanupAndSetup: cleanupAndSetupModule, errorMessages, setup: setupModule, setupStatus, - timestampField, viewResults, -}: AnalysisSetupStepsProps) => { + moduleDescriptor, + sourceConfiguration, +}: LogEntryRateSetupStepsProps) => { const { setup, cleanupAndSetup, @@ -50,10 +54,10 @@ export const AnalysisSetupSteps: React.FunctionComponent { + const sourceConfiguration: ModuleSourceConfiguration = useMemo( + () => ({ + indices: indexPattern.split(','), + sourceId, + spaceId, + timestampField, + }), + [indexPattern] + ); + + return useLogAnalysisModule({ + moduleDescriptor: logEntryRateModule, + sourceConfiguration, + }); +}; + +export const [LogEntryRateModuleProvider, useLogEntryRateModuleContext] = createContainer( + useLogEntryRateModule +); diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_results.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_rate_results.ts similarity index 58% rename from x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_results.tsx rename to x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_rate_results.ts index 81a80fb565a4b..de2b873001cce 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_results.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_rate_results.ts @@ -4,75 +4,77 @@ * you may not use this file except in compliance with the Elastic License. */ -import createContainer from 'constate'; -import { useMemo, useEffect } from 'react'; +import { useMemo, useState } from 'react'; -import { useLogEntryRate } from './log_entry_rate'; -import { GetLogEntryRateSuccessResponsePayload } from '../../../../common/http_api/log_analysis'; +import { + GetLogEntryRateSuccessResponsePayload, + LogEntryRateHistogramBucket, + LogEntryRatePartition, +} from '../../../../common/http_api/log_analysis'; +import { useTrackedPromise } from '../../../utils/use_tracked_promise'; +import { callGetLogEntryRateAPI } from './service_calls/get_log_entry_rate'; -type PartitionBucket = { +type PartitionBucket = LogEntryRatePartition & { startTime: number; -} & GetLogEntryRateSuccessResponsePayload['data']['histogramBuckets'][0]['partitions'][0]; +}; type PartitionRecord = Record< string, { buckets: PartitionBucket[]; topAnomalyScore: number; totalNumberOfLogEntries: number } >; -export interface LogRateResults { +export interface LogEntryRateResults { bucketDuration: number; totalNumberOfLogEntries: number; - histogramBuckets: GetLogEntryRateSuccessResponsePayload['data']['histogramBuckets']; + histogramBuckets: LogEntryRateHistogramBucket[]; partitionBuckets: PartitionRecord; } -export const useLogAnalysisResults = ({ +export const useLogEntryRateResults = ({ sourceId, startTime, endTime, bucketDuration = 15 * 60 * 1000, - lastRequestTime, }: { sourceId: string; startTime: number; endTime: number; - bucketDuration?: number; - lastRequestTime: number; + bucketDuration: number; }) => { - const { isLoading: isLoadingLogEntryRate, logEntryRate, getLogEntryRate } = useLogEntryRate({ - sourceId, - startTime, - endTime, - bucketDuration, - }); - - const isLoading = useMemo(() => isLoadingLogEntryRate, [isLoadingLogEntryRate]); + const [logEntryRate, setLogEntryRate] = useState(null); - useEffect(() => { - getLogEntryRate(); - }, [sourceId, startTime, endTime, bucketDuration, lastRequestTime]); + const [getLogEntryRateRequest, getLogEntryRate] = useTrackedPromise( + { + cancelPreviousOn: 'resolution', + createPromise: async () => { + return await callGetLogEntryRateAPI(sourceId, startTime, endTime, bucketDuration); + }, + onResolve: ({ data }) => { + setLogEntryRate({ + bucketDuration: data.bucketDuration, + totalNumberOfLogEntries: data.totalNumberOfLogEntries, + histogramBuckets: data.histogramBuckets, + partitionBuckets: formatLogEntryRateResultsByPartition(data), + }); + }, + onReject: () => { + setLogEntryRate(null); + }, + }, + [sourceId, startTime, endTime, bucketDuration] + ); - const logRateResults: LogRateResults | null = useMemo(() => { - if (logEntryRate) { - return { - bucketDuration: logEntryRate.bucketDuration, - totalNumberOfLogEntries: logEntryRate.totalNumberOfLogEntries, - histogramBuckets: logEntryRate.histogramBuckets, - partitionBuckets: formatLogEntryRateResultsByPartition(logEntryRate), - }; - } else { - return null; - } - }, [logEntryRate]); + const isLoading = useMemo(() => getLogEntryRateRequest.state === 'pending', [ + getLogEntryRateRequest.state, + ]); return { + getLogEntryRate, isLoading, - logRateResults, + logEntryRate, }; }; -export const LogAnalysisResults = createContainer(useLogAnalysisResults); - const formatLogEntryRateResultsByPartition = ( results: GetLogEntryRateSuccessResponsePayload['data'] ): PartitionRecord => { diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_results_url_state.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_rate_results_url_state.tsx similarity index 96% rename from x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_results_url_state.tsx rename to x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_rate_results_url_state.tsx index 19fb7f238fc04..017be6be49e16 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_results_url_state.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_rate_results_url_state.tsx @@ -4,11 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useEffect } from 'react'; -import * as rt from 'io-ts'; -import { identity, constant } from 'fp-ts/lib/function'; import { fold } from 'fp-ts/lib/Either'; +import { constant, identity } from 'fp-ts/lib/function'; import { pipe } from 'fp-ts/lib/pipeable'; +import * as rt from 'io-ts'; +import { useEffect } from 'react'; + import { useUrlState } from '../../../utils/use_url_state'; const autoRefreshRT = rt.union([ diff --git a/x-pack/legacy/plugins/infra/server/infra_server.ts b/x-pack/legacy/plugins/infra/server/infra_server.ts index 845e54e18c7c5..e0c8f607daa93 100644 --- a/x-pack/legacy/plugins/infra/server/infra_server.ts +++ b/x-pack/legacy/plugins/infra/server/infra_server.ts @@ -12,8 +12,8 @@ import { createSourceStatusResolvers } from './graphql/source_status'; import { createSourcesResolvers } from './graphql/sources'; import { InfraBackendLibs } from './lib/infra_types'; import { - initLogAnalysisGetLogEntryRateRoute, - initIndexPatternsValidateRoute, + initGetLogEntryRateRoute, + initValidateLogAnalysisIndicesRoute, } from './routes/log_analysis'; import { initMetricExplorerRoute } from './routes/metrics_explorer'; import { initMetadataRoute } from './routes/metadata'; @@ -33,10 +33,10 @@ export const initInfraServer = (libs: InfraBackendLibs) => { libs.framework.registerGraphQLEndpoint('/graphql', schema); initIpToHostName(libs); - initLogAnalysisGetLogEntryRateRoute(libs); + initGetLogEntryRateRoute(libs); initSnapshotRoute(libs); initNodeDetailsRoute(libs); - initIndexPatternsValidateRoute(libs); + initValidateLogAnalysisIndicesRoute(libs); initMetricExplorerRoute(libs); initMetadataRoute(libs); }; diff --git a/x-pack/legacy/plugins/infra/server/lib/adapters/framework/adapter_types.ts b/x-pack/legacy/plugins/infra/server/lib/adapters/framework/adapter_types.ts index 625607c098028..e88736b08b95b 100644 --- a/x-pack/legacy/plugins/infra/server/lib/adapters/framework/adapter_types.ts +++ b/x-pack/legacy/plugins/infra/server/lib/adapters/framework/adapter_types.ts @@ -29,12 +29,12 @@ export interface InfraServerPluginDeps { export interface CallWithRequestParams extends GenericParams { max_concurrent_shard_requests?: number; name?: string; - index?: string; + index?: string | string[]; ignore_unavailable?: boolean; allow_no_indices?: boolean; size?: number; terminate_after?: number; - fields?: string; + fields?: string | string[]; } export type InfraResponse = Lifecycle.ReturnValue; diff --git a/x-pack/legacy/plugins/infra/server/routes/log_analysis/index.ts b/x-pack/legacy/plugins/infra/server/routes/log_analysis/index.ts index 7364d167efe47..378e32cb3582c 100644 --- a/x-pack/legacy/plugins/infra/server/routes/log_analysis/index.ts +++ b/x-pack/legacy/plugins/infra/server/routes/log_analysis/index.ts @@ -5,4 +5,4 @@ */ export * from './results'; -export * from './index_patterns'; +export * from './validation'; diff --git a/x-pack/legacy/plugins/infra/server/routes/log_analysis/results/log_entry_rate.ts b/x-pack/legacy/plugins/infra/server/routes/log_analysis/results/log_entry_rate.ts index 973080c880e6d..02866e797e305 100644 --- a/x-pack/legacy/plugins/infra/server/routes/log_analysis/results/log_entry_rate.ts +++ b/x-pack/legacy/plugins/infra/server/routes/log_analysis/results/log_entry_rate.ts @@ -22,10 +22,7 @@ import { NoLogRateResultsIndexError } from '../../../lib/log_analysis'; const anyObject = schema.object({}, { allowUnknowns: true }); -export const initLogAnalysisGetLogEntryRateRoute = ({ - framework, - logAnalysis, -}: InfraBackendLibs) => { +export const initGetLogEntryRateRoute = ({ framework, logAnalysis }: InfraBackendLibs) => { framework.registerRoute( { method: 'post', diff --git a/x-pack/legacy/plugins/infra/server/routes/log_analysis/index_patterns/index.ts b/x-pack/legacy/plugins/infra/server/routes/log_analysis/validation/index.ts similarity index 89% rename from x-pack/legacy/plugins/infra/server/routes/log_analysis/index_patterns/index.ts rename to x-pack/legacy/plugins/infra/server/routes/log_analysis/validation/index.ts index a85e119e7318a..727faca69298e 100644 --- a/x-pack/legacy/plugins/infra/server/routes/log_analysis/index_patterns/index.ts +++ b/x-pack/legacy/plugins/infra/server/routes/log_analysis/validation/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './validate'; +export * from './indices'; diff --git a/x-pack/legacy/plugins/infra/server/routes/log_analysis/index_patterns/validate.ts b/x-pack/legacy/plugins/infra/server/routes/log_analysis/validation/indices.ts similarity index 77% rename from x-pack/legacy/plugins/infra/server/routes/log_analysis/index_patterns/validate.ts rename to x-pack/legacy/plugins/infra/server/routes/log_analysis/validation/indices.ts index 1f64da1859b5f..ba143a597b66d 100644 --- a/x-pack/legacy/plugins/infra/server/routes/log_analysis/index_patterns/validate.ts +++ b/x-pack/legacy/plugins/infra/server/routes/log_analysis/validation/indices.ts @@ -11,7 +11,7 @@ import { identity } from 'fp-ts/lib/function'; import { schema } from '@kbn/config-schema'; import { InfraBackendLibs } from '../../../lib/infra_types'; import { - LOG_ANALYSIS_VALIDATION_INDICES_PATH, + LOG_ANALYSIS_VALIDATE_INDICES_PATH, validationIndicesRequestPayloadRT, validationIndicesResponsePayloadRT, ValidationIndicesError, @@ -19,14 +19,13 @@ import { import { throwErrors } from '../../../../common/runtime_types'; -const partitionField = 'event.dataset'; const escapeHatch = schema.object({}, { allowUnknowns: true }); -export const initIndexPatternsValidateRoute = ({ framework }: InfraBackendLibs) => { +export const initValidateLogAnalysisIndicesRoute = ({ framework }: InfraBackendLibs) => { framework.registerRoute( { method: 'post', - path: LOG_ANALYSIS_VALIDATION_INDICES_PATH, + path: LOG_ANALYSIS_VALIDATE_INDICES_PATH, validate: { body: escapeHatch }, }, async (requestContext, request, response) => { @@ -36,7 +35,7 @@ export const initIndexPatternsValidateRoute = ({ framework }: InfraBackendLibs) fold(throwErrors(Boom.badRequest), identity) ); - const { timestampField, indices } = payload.data; + const { fields, indices } = payload.data; const errors: ValidationIndicesError[] = []; // Query each pattern individually, to map correctly the errors @@ -44,7 +43,7 @@ export const initIndexPatternsValidateRoute = ({ framework }: InfraBackendLibs) indices.map(async index => { const fieldCaps = await framework.callWithRequest(requestContext, 'fieldCaps', { index, - fields: `${timestampField},${partitionField}`, + fields: fields.map(field => field.name), }); if (fieldCaps.indices.length === 0) { @@ -55,32 +54,30 @@ export const initIndexPatternsValidateRoute = ({ framework }: InfraBackendLibs) return; } - ([ - [timestampField, 'date'], - [partitionField, 'keyword'], - ] as const).forEach(([field, fieldType]) => { - const fieldMetadata = fieldCaps.fields[field]; + fields.forEach(({ name: fieldName, validTypes }) => { + const fieldMetadata = fieldCaps.fields[fieldName]; if (fieldMetadata === undefined) { errors.push({ error: 'FIELD_NOT_FOUND', index, - field, + field: fieldName, }); } else { const fieldTypes = Object.keys(fieldMetadata); - if (fieldTypes.length > 1 || fieldTypes[0] !== fieldType) { + if (!fieldTypes.every(fieldType => validTypes.includes(fieldType))) { errors.push({ error: `FIELD_NOT_VALID`, index, - field, + field: fieldName, }); } } }); }) ); + return response.ok({ body: validationIndicesResponsePayloadRT.encode({ data: { errors } }), }); From 48d897e6e7127c28b92be1fc210c06d268a2d982 Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Tue, 10 Dec 2019 12:01:48 -0700 Subject: [PATCH 08/40] [SIEM][Detection Engine] Adds the default name space to the end of the signals index ## Summary One liner to add the `default` to the end of the siem signals index for people to play with it. ### Checklist Use ~~strikethroughs~~ to remove checklist items you don't feel are applicable to this PR. ~~- [ ] This was checked for cross-browser compatibility, [including a check against IE11](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility)~~ ~~- [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/master/packages/kbn-i18n/README.md)~~ ~~- [ ] [Documentation](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#writing-documentation) was added for features that require explanation or tutorials~~ ~~- [ ] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios~~ ~~- [ ] This was checked for [keyboard-only and screenreader accessibility](https://developer.mozilla.org/en-US/docs/Learn/Tools_and_testing/Cross_browser_testing/Accessibility#Accessibility_testing_checklist)~~ ### For maintainers ~~- [ ] This was checked for breaking API changes and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process)~~ ~~- [ ] This includes a feature addition or change that requires a release note and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process)~~ --- .../siem/public/pages/detection_engine/signals/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/index.tsx index ca178db9cd97f..74b7b9349c2cb 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/index.tsx @@ -64,7 +64,7 @@ export const SignalsTable = React.memo(() => { {({ to, from, setQuery, deleteQuery, isInitializing }) => ( Date: Tue, 10 Dec 2019 12:55:41 -0700 Subject: [PATCH 09/40] [Telemetry/Pulse] Updates advanced settings text for usage data (#52657) * [Telemetry/Pulse] Updates advanced settings text for usage data --- .../__snapshots__/telemetry_form.test.js.snap | 31 +++++++++++-------- .../public/components/telemetry_form.js | 28 +++++++++++------ 2 files changed, 36 insertions(+), 23 deletions(-) diff --git a/src/legacy/core_plugins/telemetry/public/components/__snapshots__/telemetry_form.test.js.snap b/src/legacy/core_plugins/telemetry/public/components/__snapshots__/telemetry_form.test.js.snap index a7f8d72e016f8..079a43e77616d 100644 --- a/src/legacy/core_plugins/telemetry/public/components/__snapshots__/telemetry_form.test.js.snap +++ b/src/legacy/core_plugins/telemetry/public/components/__snapshots__/telemetry_form.test.js.snap @@ -38,7 +38,24 @@ exports[`TelemetryForm renders as expected when allows to change optIn status 1` "defVal": true, "description":

- Help us improve the Elastic Stack by providing usage statistics for basic features. We will not share this data outside of Elastic. + + + , + } + } + />

-

- - - -

, "type": "boolean", "value": false, diff --git a/src/legacy/core_plugins/telemetry/public/components/telemetry_form.js b/src/legacy/core_plugins/telemetry/public/components/telemetry_form.js index d4bbe1029b40d..f6012a271cde5 100644 --- a/src/legacy/core_plugins/telemetry/public/components/telemetry_form.js +++ b/src/legacy/core_plugins/telemetry/public/components/telemetry_form.js @@ -29,7 +29,7 @@ import { EuiSpacer, EuiText, } from '@elastic/eui'; -import { getConfigTelemetryDesc, PRIVACY_STATEMENT_URL } from '../../common/constants'; +import { PRIVACY_STATEMENT_URL } from '../../common/constants'; import { OptInExampleFlyout } from './opt_in_details_component'; import { Field } from 'ui/management'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -162,7 +162,23 @@ export class TelemetryForm extends Component { renderDescription = () => ( -

{getConfigTelemetryDesc()}

+

+ + + + ) + }} + /> +

-

- - - -

) From 6e476e845d38018a1f069edadd81065ac5490dd0 Mon Sep 17 00:00:00 2001 From: gchaps <33642766+gchaps@users.noreply.github.com> Date: Tue, 10 Dec 2019 12:30:11 -0800 Subject: [PATCH 10/40] [DOCS] Updtes description of elasticsearch.requestHeadersWhitelist (#52675) --- docs/setup/settings.asciidoc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index 39c87d97af4ba..5cda7b2b214f0 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -65,6 +65,8 @@ connects to this Kibana instance. `elasticsearch.requestHeadersWhitelist:`:: *Default: `[ 'authorization' ]`* List of Kibana client-side headers to send to Elasticsearch. To send *no* client-side headers, set this value to [] (an empty list). +Removing the `authorization` header from being whitelisted means that you cannot +use <> in Kibana. `elasticsearch.requestTimeout:`:: *Default: 30000* Time in milliseconds to wait for responses from the back end or Elasticsearch. This value must be a positive From 0eb4c18fe09c3f01bee3ec16206738a3a53f78ae Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Tue, 10 Dec 2019 22:05:19 +0000 Subject: [PATCH 11/40] feat(NA): add trap for SIGINT in the git precommit hook (#52662) --- src/dev/register_git_hook/register_git_hook.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/dev/register_git_hook/register_git_hook.js b/src/dev/register_git_hook/register_git_hook.js index a61922078e687..31136cab0adae 100644 --- a/src/dev/register_git_hook/register_git_hook.js +++ b/src/dev/register_git_hook/register_git_hook.js @@ -58,6 +58,15 @@ function getKbnPrecommitGitHookScript(rootPath, nodeHome, platform) { set -euo pipefail + # Make it possible to terminate pre commit hook + # using ctrl-c so nothing else would happen or be + # sent to the output. + # + # The correct exit code on that situation + # according the linux documentation project is 130 + # https://www.tldp.org/LDP/abs/html/exitcodes.html + trap "exit 130" SIGINT + has_node() { command -v node >/dev/null 2>&1 } From 79fc07c0c3505a0d45ac00aca7573cb21ef3c531 Mon Sep 17 00:00:00 2001 From: Stacey Gammon Date: Tue, 10 Dec 2019 18:02:03 -0500 Subject: [PATCH 12/40] Add top level examples folder and command to run, `--run-examples`. (#52027) * Add top level examples folder and command to run, `--run-examples`. * Add comment explaining reason --run-examples flag turns off base path. --- .ci/packer_cache.sh | 1 + examples/README.md | 8 +++ examples/demo_search/README.md | 8 +++ .../demo_search/common/index.ts | 5 +- .../demo_search/kibana.json | 0 .../demo_search/package.json | 2 +- .../public/demo_search_strategy.ts | 4 +- .../demo_search/public/index.ts | 0 .../demo_search/public/plugin.ts | 6 +- .../server/demo_search_strategy.ts | 2 +- .../demo_search/server/index.ts | 0 .../demo_search/server/plugin.ts | 2 +- .../demo_search/tsconfig.json | 4 +- examples/search_explorer/README.md | 8 +++ .../search_explorer/kibana.json | 0 .../search_explorer/package.json | 2 +- .../search_explorer/public/application.tsx | 2 +- .../search_explorer/public/demo_strategy.tsx | 2 +- .../search_explorer/public/do_search.tsx | 5 +- .../search_explorer/public/documentation.tsx | 0 .../search_explorer/public/es_strategy.tsx | 10 ++-- .../search_explorer/public/guide_section.tsx | 0 .../search_explorer/public/index.ts | 0 .../search_explorer/public/page.tsx | 0 .../search_explorer/public/plugin.tsx | 2 +- .../search_explorer/public/search_api.tsx | 16 +++--- .../search_explorer/tsconfig.json | 4 +- package.json | 1 + packages/kbn-pm/dist/index.js | 1 + packages/kbn-pm/src/config.ts | 1 + renovate.json5 | 1 + scripts/functional_tests.js | 1 + src/cli/serve/serve.js | 15 ++++- src/dev/renovate/package_globs.ts | 1 + src/dev/typescript/projects.ts | 3 + test/examples/README.md | 23 ++++++++ test/examples/config.js | 55 +++++++++++++++++++ .../search/demo_data.ts | 0 .../search/es_search.ts | 0 .../test_suites => examples}/search/index.ts | 0 test/plugin_functional/config.js | 1 - 41 files changed, 156 insertions(+), 40 deletions(-) create mode 100644 examples/README.md create mode 100644 examples/demo_search/README.md rename {test/plugin_functional/plugins => examples}/demo_search/common/index.ts (90%) rename {test/plugin_functional/plugins => examples}/demo_search/kibana.json (100%) rename {test/plugin_functional/plugins => examples}/demo_search/package.json (88%) rename {test/plugin_functional/plugins => examples}/demo_search/public/demo_search_strategy.ts (96%) rename {test/plugin_functional/plugins => examples}/demo_search/public/index.ts (100%) rename {test/plugin_functional/plugins => examples}/demo_search/public/plugin.ts (92%) rename {test/plugin_functional/plugins => examples}/demo_search/server/demo_search_strategy.ts (94%) rename {test/plugin_functional/plugins => examples}/demo_search/server/index.ts (100%) rename {test/plugin_functional/plugins => examples}/demo_search/server/plugin.ts (97%) rename {test/plugin_functional/plugins => examples}/demo_search/tsconfig.json (75%) create mode 100644 examples/search_explorer/README.md rename {test/plugin_functional/plugins => examples}/search_explorer/kibana.json (100%) rename {test/plugin_functional/plugins => examples}/search_explorer/package.json (87%) rename {test/plugin_functional/plugins => examples}/search_explorer/public/application.tsx (97%) rename {test/plugin_functional/plugins => examples}/search_explorer/public/demo_strategy.tsx (98%) rename {test/plugin_functional/plugins => examples}/search_explorer/public/do_search.tsx (97%) rename {test/plugin_functional/plugins => examples}/search_explorer/public/documentation.tsx (100%) rename {test/plugin_functional/plugins => examples}/search_explorer/public/es_strategy.tsx (87%) rename {test/plugin_functional/plugins => examples}/search_explorer/public/guide_section.tsx (100%) rename {test/plugin_functional/plugins => examples}/search_explorer/public/index.ts (100%) rename {test/plugin_functional/plugins => examples}/search_explorer/public/page.tsx (100%) rename {test/plugin_functional/plugins => examples}/search_explorer/public/plugin.tsx (94%) rename {test/plugin_functional/plugins => examples}/search_explorer/public/search_api.tsx (70%) rename {test/plugin_functional/plugins => examples}/search_explorer/tsconfig.json (73%) create mode 100644 test/examples/README.md create mode 100644 test/examples/config.js rename test/{plugin_functional/test_suites => examples}/search/demo_data.ts (100%) rename test/{plugin_functional/test_suites => examples}/search/es_search.ts (100%) rename test/{plugin_functional/test_suites => examples}/search/index.ts (100%) diff --git a/.ci/packer_cache.sh b/.ci/packer_cache.sh index b697f22c009d1..ab68a60dcfc27 100755 --- a/.ci/packer_cache.sh +++ b/.ci/packer_cache.sh @@ -44,6 +44,7 @@ tar -cf "$HOME/.kibana/bootstrap_cache/$branch.tar" \ x-pack/legacy/plugins/*/node_modules \ x-pack/legacy/plugins/reporting/.chromium \ test/plugin_functional/plugins/*/node_modules \ + examples/*/node_modules \ .es \ .chromedriver \ .geckodriver; diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000000000..7cade0b35f820 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,8 @@ +## Example plugins + +This folder contains example plugins. To run the plugins in this folder, use the `--run-examples` flag, via + +``` +yarn start --run-examples +``` + diff --git a/examples/demo_search/README.md b/examples/demo_search/README.md new file mode 100644 index 0000000000000..f0b461e3287b4 --- /dev/null +++ b/examples/demo_search/README.md @@ -0,0 +1,8 @@ +## Demo search strategy + +This example registers a custom search strategy that simply takes a name string in the request and returns the +string `Hello {name}` + +To see the demo search strategy in action, navigate to the `Search explorer` app. + +To run these examples, use the command `yarn start --run-examples`. \ No newline at end of file diff --git a/test/plugin_functional/plugins/demo_search/common/index.ts b/examples/demo_search/common/index.ts similarity index 90% rename from test/plugin_functional/plugins/demo_search/common/index.ts rename to examples/demo_search/common/index.ts index 9254412ece291..6587ee96ef61b 100644 --- a/test/plugin_functional/plugins/demo_search/common/index.ts +++ b/examples/demo_search/common/index.ts @@ -17,10 +17,7 @@ * under the License. */ -import { - IKibanaSearchRequest, - IKibanaSearchResponse, -} from '../../../../../src/plugins/data/public'; +import { IKibanaSearchRequest, IKibanaSearchResponse } from '../../../src/plugins/data/public'; export const DEMO_SEARCH_STRATEGY = 'DEMO_SEARCH_STRATEGY'; diff --git a/test/plugin_functional/plugins/demo_search/kibana.json b/examples/demo_search/kibana.json similarity index 100% rename from test/plugin_functional/plugins/demo_search/kibana.json rename to examples/demo_search/kibana.json diff --git a/test/plugin_functional/plugins/demo_search/package.json b/examples/demo_search/package.json similarity index 88% rename from test/plugin_functional/plugins/demo_search/package.json rename to examples/demo_search/package.json index 1f4fa1421906a..404002a50e710 100644 --- a/test/plugin_functional/plugins/demo_search/package.json +++ b/examples/demo_search/package.json @@ -8,7 +8,7 @@ }, "license": "Apache-2.0", "scripts": { - "kbn": "node ../../../../scripts/kbn.js", + "kbn": "node ../../scripts/kbn.js", "build": "rm -rf './target' && tsc" }, "devDependencies": { diff --git a/test/plugin_functional/plugins/demo_search/public/demo_search_strategy.ts b/examples/demo_search/public/demo_search_strategy.ts similarity index 96% rename from test/plugin_functional/plugins/demo_search/public/demo_search_strategy.ts rename to examples/demo_search/public/demo_search_strategy.ts index 298eaaaf420e0..d2854151e14c8 100644 --- a/test/plugin_functional/plugins/demo_search/public/demo_search_strategy.ts +++ b/examples/demo_search/public/demo_search_strategy.ts @@ -22,8 +22,8 @@ import { ISearchContext, SYNC_SEARCH_STRATEGY, ISearchGeneric, -} from '../../../../../src/plugins/data/public'; -import { TSearchStrategyProvider, ISearchStrategy } from '../../../../../src/plugins/data/public'; +} from '../../../src/plugins/data/public'; +import { TSearchStrategyProvider, ISearchStrategy } from '../../../src/plugins/data/public'; import { DEMO_SEARCH_STRATEGY, IDemoResponse } from '../common'; diff --git a/test/plugin_functional/plugins/demo_search/public/index.ts b/examples/demo_search/public/index.ts similarity index 100% rename from test/plugin_functional/plugins/demo_search/public/index.ts rename to examples/demo_search/public/index.ts diff --git a/test/plugin_functional/plugins/demo_search/public/plugin.ts b/examples/demo_search/public/plugin.ts similarity index 92% rename from test/plugin_functional/plugins/demo_search/public/plugin.ts rename to examples/demo_search/public/plugin.ts index 37f8d3955708a..81ba585b99190 100644 --- a/test/plugin_functional/plugins/demo_search/public/plugin.ts +++ b/examples/demo_search/public/plugin.ts @@ -17,8 +17,8 @@ * under the License. */ -import { DataPublicPluginSetup } from '../../../../../src/plugins/data/public'; -import { Plugin, CoreSetup, PluginInitializerContext } from '../../../../../src/core/public'; +import { DataPublicPluginSetup } from '../../../src/plugins/data/public'; +import { Plugin, CoreSetup, PluginInitializerContext } from '../../../src/core/public'; import { DEMO_SEARCH_STRATEGY } from '../common'; import { demoClientSearchStrategyProvider } from './demo_search_strategy'; import { IDemoRequest, IDemoResponse } from '../common'; @@ -36,7 +36,7 @@ interface DemoDataSearchSetupDependencies { * If the caller does not pass in the right `request` shape, typescript will * complain. The caller will also get a typed response. */ -declare module '../../../../../src/plugins/data/public' { +declare module '../../../src/plugins/data/public' { export interface IRequestTypesMap { [DEMO_SEARCH_STRATEGY]: IDemoRequest; } diff --git a/test/plugin_functional/plugins/demo_search/server/demo_search_strategy.ts b/examples/demo_search/server/demo_search_strategy.ts similarity index 94% rename from test/plugin_functional/plugins/demo_search/server/demo_search_strategy.ts rename to examples/demo_search/server/demo_search_strategy.ts index d3f2360add6c0..5b0883be1fc51 100644 --- a/test/plugin_functional/plugins/demo_search/server/demo_search_strategy.ts +++ b/examples/demo_search/server/demo_search_strategy.ts @@ -17,7 +17,7 @@ * under the License. */ -import { TSearchStrategyProvider } from 'src/plugins/data/server'; +import { TSearchStrategyProvider } from '../../../src/plugins/data/server'; import { DEMO_SEARCH_STRATEGY } from '../common'; export const demoSearchStrategyProvider: TSearchStrategyProvider = () => { diff --git a/test/plugin_functional/plugins/demo_search/server/index.ts b/examples/demo_search/server/index.ts similarity index 100% rename from test/plugin_functional/plugins/demo_search/server/index.ts rename to examples/demo_search/server/index.ts diff --git a/test/plugin_functional/plugins/demo_search/server/plugin.ts b/examples/demo_search/server/plugin.ts similarity index 97% rename from test/plugin_functional/plugins/demo_search/server/plugin.ts rename to examples/demo_search/server/plugin.ts index c6628e7c76820..23c82225563c8 100644 --- a/test/plugin_functional/plugins/demo_search/server/plugin.ts +++ b/examples/demo_search/server/plugin.ts @@ -35,7 +35,7 @@ interface IDemoSearchExplorerDeps { * If the caller does not pass in the right `request` shape, typescript will * complain. The caller will also get a typed response. */ -declare module '../../../../../src/plugins/data/server' { +declare module '../../../src/plugins/data/server' { export interface IRequestTypesMap { [DEMO_SEARCH_STRATEGY]: IDemoRequest; } diff --git a/test/plugin_functional/plugins/demo_search/tsconfig.json b/examples/demo_search/tsconfig.json similarity index 75% rename from test/plugin_functional/plugins/demo_search/tsconfig.json rename to examples/demo_search/tsconfig.json index 304ffdc0a299d..7fa03739119b4 100644 --- a/test/plugin_functional/plugins/demo_search/tsconfig.json +++ b/examples/demo_search/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../../tsconfig.json", + "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "./target", "skipLibCheck": true @@ -10,7 +10,7 @@ "public/**/*.ts", "public/**/*.tsx", "server/**/*.ts", - "../../../../typings/**/*" + "../../typings/**/*" ], "exclude": [] } diff --git a/examples/search_explorer/README.md b/examples/search_explorer/README.md new file mode 100644 index 0000000000000..0e5a48cf22dc1 --- /dev/null +++ b/examples/search_explorer/README.md @@ -0,0 +1,8 @@ +## Search explorer + +This example search explorer app shows how to use different search strategies in order to retrieve data. + +One demo uses the built in elasticsearch search strategy, and runs a search against data in elasticsearch. The +other demo uses the custom demo search strategy, a custom search strategy registerd inside the [demo_search plugin](../demo_search). + +To run this example, use the command `yarn start --run-examples`. \ No newline at end of file diff --git a/test/plugin_functional/plugins/search_explorer/kibana.json b/examples/search_explorer/kibana.json similarity index 100% rename from test/plugin_functional/plugins/search_explorer/kibana.json rename to examples/search_explorer/kibana.json diff --git a/test/plugin_functional/plugins/search_explorer/package.json b/examples/search_explorer/package.json similarity index 87% rename from test/plugin_functional/plugins/search_explorer/package.json rename to examples/search_explorer/package.json index 9a5e0e83a2207..62d0127c30cc6 100644 --- a/test/plugin_functional/plugins/search_explorer/package.json +++ b/examples/search_explorer/package.json @@ -8,7 +8,7 @@ }, "license": "Apache-2.0", "scripts": { - "kbn": "node ../../../../scripts/kbn.js", + "kbn": "node ../../scripts/kbn.js", "build": "rm -rf './target' && tsc" }, "devDependencies": { diff --git a/test/plugin_functional/plugins/search_explorer/public/application.tsx b/examples/search_explorer/public/application.tsx similarity index 97% rename from test/plugin_functional/plugins/search_explorer/public/application.tsx rename to examples/search_explorer/public/application.tsx index 4762209a548c1..801a3c615ac61 100644 --- a/test/plugin_functional/plugins/search_explorer/public/application.tsx +++ b/examples/search_explorer/public/application.tsx @@ -28,7 +28,7 @@ import { EuiSideNav, } from '@elastic/eui'; -import { AppMountContext, AppMountParameters } from '../../../../../src/core/public'; +import { AppMountContext, AppMountParameters } from '../../../src/core/public'; import { EsSearchTest } from './es_strategy'; import { Page } from './page'; import { DemoStrategy } from './demo_strategy'; diff --git a/test/plugin_functional/plugins/search_explorer/public/demo_strategy.tsx b/examples/search_explorer/public/demo_strategy.tsx similarity index 98% rename from test/plugin_functional/plugins/search_explorer/public/demo_strategy.tsx rename to examples/search_explorer/public/demo_strategy.tsx index 8a0dd31e3595f..7c6c21d2b7aed 100644 --- a/test/plugin_functional/plugins/search_explorer/public/demo_strategy.tsx +++ b/examples/search_explorer/public/demo_strategy.tsx @@ -25,7 +25,7 @@ import { EuiFlexGroup, EuiFieldText, } from '@elastic/eui'; -import { ISearchGeneric } from '../../../../../src/plugins/data/public'; +import { ISearchGeneric } from '../../../src/plugins/data/public'; import { DoSearch } from './do_search'; import { GuideSection } from './guide_section'; diff --git a/test/plugin_functional/plugins/search_explorer/public/do_search.tsx b/examples/search_explorer/public/do_search.tsx similarity index 97% rename from test/plugin_functional/plugins/search_explorer/public/do_search.tsx rename to examples/search_explorer/public/do_search.tsx index e039e4ff3f63f..f279b9fcd6e23 100644 --- a/test/plugin_functional/plugins/search_explorer/public/do_search.tsx +++ b/examples/search_explorer/public/do_search.tsx @@ -21,10 +21,7 @@ import React from 'react'; import { EuiButton, EuiCodeBlock, EuiFlexItem, EuiFlexGroup, EuiText } from '@elastic/eui'; import { EuiProgress } from '@elastic/eui'; import { Observable } from 'rxjs'; -import { - IKibanaSearchResponse, - IKibanaSearchRequest, -} from '../../../../../src/plugins/data/public'; +import { IKibanaSearchResponse, IKibanaSearchRequest } from '../../../src/plugins/data/public'; interface Props { request: IKibanaSearchRequest; diff --git a/test/plugin_functional/plugins/search_explorer/public/documentation.tsx b/examples/search_explorer/public/documentation.tsx similarity index 100% rename from test/plugin_functional/plugins/search_explorer/public/documentation.tsx rename to examples/search_explorer/public/documentation.tsx diff --git a/test/plugin_functional/plugins/search_explorer/public/es_strategy.tsx b/examples/search_explorer/public/es_strategy.tsx similarity index 87% rename from test/plugin_functional/plugins/search_explorer/public/es_strategy.tsx rename to examples/search_explorer/public/es_strategy.tsx index d35c67191a1f8..e26c11a646669 100644 --- a/test/plugin_functional/plugins/search_explorer/public/es_strategy.tsx +++ b/examples/search_explorer/public/es_strategy.tsx @@ -29,19 +29,19 @@ import { ISearchGeneric, IEsSearchResponse, IEsSearchRequest, -} from '../../../../../src/plugins/data/public'; +} from '../../../src/plugins/data/public'; import { DoSearch } from './do_search'; import { GuideSection } from './guide_section'; // @ts-ignore -import serverPlugin from '!!raw-loader!./../../../../../src/plugins/data/server/search/es_search/es_search_service'; +import serverPlugin from '!!raw-loader!./../../../src/plugins/data/server/search/es_search/es_search_service'; // @ts-ignore -import serverStrategy from '!!raw-loader!./../../../../../src/plugins/data/server/search/es_search/es_search_strategy'; +import serverStrategy from '!!raw-loader!./../../../src/plugins/data/server/search/es_search/es_search_strategy'; // @ts-ignore -import publicPlugin from '!!raw-loader!./../../../../../src/plugins/data/public/search/es_search/es_search_service'; +import publicPlugin from '!!raw-loader!./../../../src/plugins/data/public/search/es_search/es_search_service'; // @ts-ignore -import publicStrategy from '!!raw-loader!./../../../../../src/plugins/data/public/search/es_search/es_search_strategy'; +import publicStrategy from '!!raw-loader!./../../../src/plugins/data/public/search/es_search/es_search_strategy'; interface Props { search: ISearchGeneric; diff --git a/test/plugin_functional/plugins/search_explorer/public/guide_section.tsx b/examples/search_explorer/public/guide_section.tsx similarity index 100% rename from test/plugin_functional/plugins/search_explorer/public/guide_section.tsx rename to examples/search_explorer/public/guide_section.tsx diff --git a/test/plugin_functional/plugins/search_explorer/public/index.ts b/examples/search_explorer/public/index.ts similarity index 100% rename from test/plugin_functional/plugins/search_explorer/public/index.ts rename to examples/search_explorer/public/index.ts diff --git a/test/plugin_functional/plugins/search_explorer/public/page.tsx b/examples/search_explorer/public/page.tsx similarity index 100% rename from test/plugin_functional/plugins/search_explorer/public/page.tsx rename to examples/search_explorer/public/page.tsx diff --git a/test/plugin_functional/plugins/search_explorer/public/plugin.tsx b/examples/search_explorer/public/plugin.tsx similarity index 94% rename from test/plugin_functional/plugins/search_explorer/public/plugin.tsx rename to examples/search_explorer/public/plugin.tsx index cbe1073aa186b..a7a6fd11341a4 100644 --- a/test/plugin_functional/plugins/search_explorer/public/plugin.tsx +++ b/examples/search_explorer/public/plugin.tsx @@ -18,7 +18,7 @@ */ import { Plugin, CoreSetup } from 'kibana/public'; -import { ISearchAppMountContext } from '../../../../../src/plugins/data/public'; +import { ISearchAppMountContext } from '../../../src/plugins/data/public'; declare module 'kibana/public' { interface AppMountContext { diff --git a/test/plugin_functional/plugins/search_explorer/public/search_api.tsx b/examples/search_explorer/public/search_api.tsx similarity index 70% rename from test/plugin_functional/plugins/search_explorer/public/search_api.tsx rename to examples/search_explorer/public/search_api.tsx index 8ec6225d1f172..fc68571e4ef68 100644 --- a/test/plugin_functional/plugins/search_explorer/public/search_api.tsx +++ b/examples/search_explorer/public/search_api.tsx @@ -20,22 +20,22 @@ import React from 'react'; import { GuideSection } from './guide_section'; // @ts-ignore -import publicSetupContract from '!!raw-loader!./../../../../../src/plugins/data/public/search/i_search_setup'; +import publicSetupContract from '!!raw-loader!./../../../src/plugins/data/public/search/i_search_setup'; // @ts-ignore -import publicSearchStrategy from '!!raw-loader!./../../../../../src/plugins/data/public/search/i_search_strategy'; +import publicSearchStrategy from '!!raw-loader!./../../../src/plugins/data/public/search/i_search_strategy'; // @ts-ignore -import publicSearch from '!!raw-loader!./../../../../../src/plugins/data/public/search/i_search'; +import publicSearch from '!!raw-loader!./../../../src/plugins/data/public/search/i_search'; // @ts-ignore -import publicPlugin from '!!raw-loader!./../../../../../src/plugins/data/public/search/search_service'; +import publicPlugin from '!!raw-loader!./../../../src/plugins/data/public/search/search_service'; // @ts-ignore -import serverSetupContract from '!!raw-loader!./../../../../../src/plugins/data/server/search/i_search_setup'; +import serverSetupContract from '!!raw-loader!./../../../src/plugins/data/server/search/i_search_setup'; // @ts-ignore -import serverSearchStrategy from '!!raw-loader!./../../../../../src/plugins/data/server/search/i_search_strategy'; +import serverSearchStrategy from '!!raw-loader!./../../../src/plugins/data/server/search/i_search_strategy'; // @ts-ignore -import serverSearch from '!!raw-loader!./../../../../../src/plugins/data/server/search/i_search'; +import serverSearch from '!!raw-loader!./../../../src/plugins/data/server/search/i_search'; // @ts-ignore -import serverPlugin from '!!raw-loader!./../../../../../src/plugins/data/server/search/search_service'; +import serverPlugin from '!!raw-loader!./../../../src/plugins/data/server/search/search_service'; export const SearchApiPage = () => ( new Project(resolve(REPO_ROOT, path))), + ...glob + .sync('examples/*/tsconfig.json', { cwd: REPO_ROOT }) + .map(path => new Project(resolve(REPO_ROOT, path))), ...glob .sync('test/plugin_functional/plugins/*/tsconfig.json', { cwd: REPO_ROOT }) .map(path => new Project(resolve(REPO_ROOT, path))), diff --git a/test/examples/README.md b/test/examples/README.md new file mode 100644 index 0000000000000..44656f949bc72 --- /dev/null +++ b/test/examples/README.md @@ -0,0 +1,23 @@ +# Example plugin functional tests + +This folder contains functional tests for the example plugins. + +## Run the test + +To run these tests during development you can use the following commands: + +``` +# Start the test server (can continue running) +node scripts/functional_tests_server.js --config test/examples/config.js +# Start a test run +node scripts/functional_test_runner.js --config test/examples/config.js +``` + +## Run Kibana with a test plugin + +In case you want to start Kibana with the example plugins, you can just run: + +``` +yarn start --run-examples +``` + diff --git a/test/examples/config.js b/test/examples/config.js new file mode 100644 index 0000000000000..b954390dc54ad --- /dev/null +++ b/test/examples/config.js @@ -0,0 +1,55 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import path from 'path'; +import { services } from '../plugin_functional/services'; + +export default async function ({ readConfigFile }) { + const functionalConfig = await readConfigFile(require.resolve('../functional/config')); + + return { + testFiles: [ + require.resolve('./search'), + ], + services: { + ...functionalConfig.get('services'), + ...services, + }, + pageObjects: functionalConfig.get('pageObjects'), + servers: functionalConfig.get('servers'), + esTestCluster: functionalConfig.get('esTestCluster'), + apps: functionalConfig.get('apps'), + esArchiver: { + directory: path.resolve(__dirname, '../es_archives') + }, + screenshots: functionalConfig.get('screenshots'), + junit: { + reportName: 'Example plugin functional tests', + }, + kbnTestServer: { + ...functionalConfig.get('kbnTestServer'), + serverArgs: [ + ...functionalConfig.get('kbnTestServer.serverArgs'), + '--run-examples', + // Required to run examples + '--env.name=development', + ], + }, + }; +} diff --git a/test/plugin_functional/test_suites/search/demo_data.ts b/test/examples/search/demo_data.ts similarity index 100% rename from test/plugin_functional/test_suites/search/demo_data.ts rename to test/examples/search/demo_data.ts diff --git a/test/plugin_functional/test_suites/search/es_search.ts b/test/examples/search/es_search.ts similarity index 100% rename from test/plugin_functional/test_suites/search/es_search.ts rename to test/examples/search/es_search.ts diff --git a/test/plugin_functional/test_suites/search/index.ts b/test/examples/search/index.ts similarity index 100% rename from test/plugin_functional/test_suites/search/index.ts rename to test/examples/search/index.ts diff --git a/test/plugin_functional/config.js b/test/plugin_functional/config.js index a6316c607a7c7..d8ce12d1fc612 100644 --- a/test/plugin_functional/config.js +++ b/test/plugin_functional/config.js @@ -33,7 +33,6 @@ export default async function ({ readConfigFile }) { require.resolve('./test_suites/app_plugins'), require.resolve('./test_suites/custom_visualizations'), require.resolve('./test_suites/panel_actions'), - require.resolve('./test_suites/search'), /** * @todo Work on re-enabling this test suite after this is merged. These tests pass From 3e1915d287bb4c78239b1ef71849172009899317 Mon Sep 17 00:00:00 2001 From: spalger Date: Tue, 10 Dec 2019 21:07:57 -0700 Subject: [PATCH 13/40] fix newlines in kbn-analytics build script --- packages/kbn-analytics/scripts/build.js | 190 ++++++++++++------------ packages/kbn-i18n/scripts/build.js | 2 +- 2 files changed, 96 insertions(+), 96 deletions(-) diff --git a/packages/kbn-analytics/scripts/build.js b/packages/kbn-analytics/scripts/build.js index 3736ab15260fa..b7fbe629246ec 100644 --- a/packages/kbn-analytics/scripts/build.js +++ b/packages/kbn-analytics/scripts/build.js @@ -1,95 +1,95 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -const { resolve } = require('path'); - -const del = require('del'); -const supportsColor = require('supports-color'); -const { run, withProcRunner } = require('@kbn/dev-utils'); - -const ROOT_DIR = resolve(__dirname, '..'); -const BUILD_DIR = resolve(ROOT_DIR, 'target'); - -const padRight = (width, str) => - str.length >= width ? str : `${str}${' '.repeat(width - str.length)}`; - -run( - async ({ log, flags }) => { - await withProcRunner(log, async proc => { - log.info('Deleting old output'); - await del(BUILD_DIR); - - const cwd = ROOT_DIR; - const env = { ...process.env }; - if (supportsColor.stdout) { - env.FORCE_COLOR = 'true'; - } - - log.info(`Starting babel and typescript${flags.watch ? ' in watch mode' : ''}`); - await Promise.all([ - ...['web', 'node'].map(subTask => - proc.run(padRight(10, `babel:${subTask}`), { - cmd: 'babel', - args: [ - 'src', - '--config-file', - require.resolve('../babel.config.js'), - '--out-dir', - resolve(BUILD_DIR, subTask), - '--extensions', - '.ts,.js,.tsx', - ...(flags.watch ? ['--watch'] : ['--quiet']), - ...(flags['source-maps'] ? ['--source-map', 'inline'] : []), - ], - wait: true, - env: { - ...env, - BABEL_ENV: subTask, - }, - cwd, - }) - ), - - proc.run(padRight(10, 'tsc'), { - cmd: 'tsc', - args: [ - '--emitDeclarationOnly', - ...(flags.watch ? ['--watch', '--preserveWatchOutput', 'true'] : []), - ...(flags['source-maps'] ? ['--declarationMap', 'true'] : []), - ], - wait: true, - env, - cwd, - }), - ]); - - log.success('Complete'); - }); - }, - { - description: 'Simple build tool for @kbn/analytics package', - flags: { - boolean: ['watch', 'source-maps'], - help: ` - --watch Run in watch mode - --source-maps Include sourcemaps - `, - }, - } -); +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +const { resolve } = require('path'); + +const del = require('del'); +const supportsColor = require('supports-color'); +const { run, withProcRunner } = require('@kbn/dev-utils'); + +const ROOT_DIR = resolve(__dirname, '..'); +const BUILD_DIR = resolve(ROOT_DIR, 'target'); + +const padRight = (width, str) => + str.length >= width ? str : `${str}${' '.repeat(width - str.length)}`; + +run( + async ({ log, flags }) => { + await withProcRunner(log, async proc => { + log.info('Deleting old output'); + await del(BUILD_DIR); + + const cwd = ROOT_DIR; + const env = { ...process.env }; + if (supportsColor.stdout) { + env.FORCE_COLOR = 'true'; + } + + log.info(`Starting babel and typescript${flags.watch ? ' in watch mode' : ''}`); + await Promise.all([ + ...['web', 'node'].map(subTask => + proc.run(padRight(10, `babel:${subTask}`), { + cmd: 'babel', + args: [ + 'src', + '--config-file', + require.resolve('../babel.config.js'), + '--out-dir', + resolve(BUILD_DIR, subTask), + '--extensions', + '.ts,.js,.tsx', + ...(flags.watch ? ['--watch'] : ['--quiet']), + ...(flags['source-maps'] ? ['--source-maps', 'inline'] : []), + ], + wait: true, + env: { + ...env, + BABEL_ENV: subTask, + }, + cwd, + }) + ), + + proc.run(padRight(10, 'tsc'), { + cmd: 'tsc', + args: [ + '--emitDeclarationOnly', + ...(flags.watch ? ['--watch', '--preserveWatchOutput', 'true'] : []), + ...(flags['source-maps'] ? ['--declarationMap', 'true'] : []), + ], + wait: true, + env, + cwd, + }), + ]); + + log.success('Complete'); + }); + }, + { + description: 'Simple build tool for @kbn/analytics package', + flags: { + boolean: ['watch', 'source-maps'], + help: ` + --watch Run in watch mode + --source-maps Include sourcemaps + `, + }, + } +); diff --git a/packages/kbn-i18n/scripts/build.js b/packages/kbn-i18n/scripts/build.js index f4260d31d80fb..ccdddc87dbc18 100644 --- a/packages/kbn-i18n/scripts/build.js +++ b/packages/kbn-i18n/scripts/build.js @@ -55,7 +55,7 @@ run( '--extensions', '.ts,.js,.tsx', ...(flags.watch ? ['--watch'] : ['--quiet']), - ...(flags['source-maps'] ? ['--source-map', 'inline'] : []), + ...(flags['source-maps'] ? ['--source-maps', 'inline'] : []), ], wait: true, env: { From 1013271c858b4c7c6107be551ada20323f989497 Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Wed, 11 Dec 2019 10:28:54 +0300 Subject: [PATCH 14/40] [ui/public/utils] Move items into ui/vis (#52615) * [ui/public/utils] Move items into ui/vis * fix PR comments --- .../ui/public/agg_types/buckets/geo_hash.ts | 2 +- src/legacy/ui/public/utils/range.d.ts | 28 -------- .../__snapshots__/number_list.test.tsx.snap | 4 +- .../components/number_list/number_row.tsx | 4 +- .../components/number_list/range.test.ts} | 71 +++++++++---------- .../controls/components/number_list/range.ts} | 61 ++++++++-------- .../components/number_list/utils.test.ts | 4 +- .../controls/components/number_list/utils.ts | 10 +-- .../ui/public/vis/map/convert_to_geojson.js | 3 +- .../map/decode_geo_hash.test.ts} | 19 ++--- .../{utils => vis/map}/decode_geo_hash.ts | 0 src/legacy/ui/public/vis/map/kibana_map.js | 2 +- .../map/zoom_to_precision.ts} | 45 ++++++------ 13 files changed, 108 insertions(+), 145 deletions(-) delete mode 100644 src/legacy/ui/public/utils/range.d.ts rename src/legacy/ui/public/{utils/__tests__/range.js => vis/editors/default/controls/components/number_list/range.test.ts} (66%) rename src/legacy/ui/public/{utils/range.js => vis/editors/default/controls/components/number_list/range.ts} (59%) rename src/legacy/ui/public/{utils/__tests__/decode_geo_hash.test.js => vis/map/decode_geo_hash.test.ts} (75%) rename src/legacy/ui/public/{utils => vis/map}/decode_geo_hash.ts (100%) rename src/legacy/ui/public/{utils/zoom_to_precision.js => vis/map/zoom_to_precision.ts} (52%) diff --git a/src/legacy/ui/public/agg_types/buckets/geo_hash.ts b/src/legacy/ui/public/agg_types/buckets/geo_hash.ts index 700f5a048fce2..0acbaf4aa02a2 100644 --- a/src/legacy/ui/public/agg_types/buckets/geo_hash.ts +++ b/src/legacy/ui/public/agg_types/buckets/geo_hash.ts @@ -18,13 +18,13 @@ */ import { i18n } from '@kbn/i18n'; +import { geohashColumns } from 'ui/vis/map/decode_geo_hash'; import chrome from '../../chrome'; import { BucketAggType, IBucketAggConfig } from './_bucket_agg_type'; import { AutoPrecisionParamEditor } from '../../vis/editors/default/controls/auto_precision'; import { UseGeocentroidParamEditor } from '../../vis/editors/default/controls/use_geocentroid'; import { IsFilteredByCollarParamEditor } from '../../vis/editors/default/controls/is_filtered_by_collar'; import { PrecisionParamEditor } from '../../vis/editors/default/controls/precision'; -import { geohashColumns } from '../../utils/decode_geo_hash'; import { AggGroupNames } from '../../vis/editors/default/agg_groups'; import { KBN_FIELD_TYPES } from '../../../../../plugins/data/public'; diff --git a/src/legacy/ui/public/utils/range.d.ts b/src/legacy/ui/public/utils/range.d.ts deleted file mode 100644 index c484c6f43eebb..0000000000000 --- a/src/legacy/ui/public/utils/range.d.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export function parseRange(input: string): Range; - -export interface Range { - min: number; - max: number; - minInclusive: boolean; - maxInclusive: boolean; - within(n: number): boolean; -} diff --git a/src/legacy/ui/public/vis/editors/default/controls/components/number_list/__snapshots__/number_list.test.tsx.snap b/src/legacy/ui/public/vis/editors/default/controls/components/number_list/__snapshots__/number_list.test.tsx.snap index ab192e6fd3cbb..4004f8627a898 100644 --- a/src/legacy/ui/public/vis/editors/default/controls/components/number_list/__snapshots__/number_list.test.tsx.snap +++ b/src/legacy/ui/public/vis/editors/default/controls/components/number_list/__snapshots__/number_list.test.tsx.snap @@ -18,7 +18,7 @@ exports[`NumberList should be rendered with default set of props 1`] = ` onChange={[Function]} onDelete={[Function]} range={ - Range { + NumberListRange { "max": 10, "maxInclusive": true, "min": 1, @@ -45,7 +45,7 @@ exports[`NumberList should be rendered with default set of props 1`] = ` onChange={[Function]} onDelete={[Function]} range={ - Range { + NumberListRange { "max": 10, "maxInclusive": true, "min": 1, diff --git a/src/legacy/ui/public/vis/editors/default/controls/components/number_list/number_row.tsx b/src/legacy/ui/public/vis/editors/default/controls/components/number_list/number_row.tsx index 23e671180e980..777b0a94f0f3d 100644 --- a/src/legacy/ui/public/vis/editors/default/controls/components/number_list/number_row.tsx +++ b/src/legacy/ui/public/vis/editors/default/controls/components/number_list/number_row.tsx @@ -21,7 +21,7 @@ import React, { useCallback } from 'react'; import { EuiFieldNumber, EuiFlexGroup, EuiFlexItem, EuiButtonIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { Range } from '../../../../../../utils/range'; +import { NumberListRange } from './range'; export interface NumberRowProps { autoFocus: boolean; @@ -29,7 +29,7 @@ export interface NumberRowProps { isInvalid: boolean; labelledbyId: string; model: NumberRowModel; - range: Range; + range: NumberListRange; onBlur(): void; onChange({ id, value }: { id: string; value: string }): void; onDelete(index: string): void; diff --git a/src/legacy/ui/public/utils/__tests__/range.js b/src/legacy/ui/public/vis/editors/default/controls/components/number_list/range.test.ts similarity index 66% rename from src/legacy/ui/public/utils/__tests__/range.js rename to src/legacy/ui/public/vis/editors/default/controls/components/number_list/range.test.ts index e7947894d3e22..e9090e5b38ef7 100644 --- a/src/legacy/ui/public/utils/__tests__/range.js +++ b/src/legacy/ui/public/vis/editors/default/controls/components/number_list/range.test.ts @@ -17,32 +17,30 @@ * under the License. */ -import _ from 'lodash'; -import expect from '@kbn/expect'; -import { parseRange } from '../range'; +import { forOwn } from 'lodash'; +import { parseRange } from './range'; -describe('Range parsing utility', function () { - - it('throws an error for inputs that are not formatted properly', function () { - expect(function () { +describe('Range parsing utility', () => { + test('throws an error for inputs that are not formatted properly', () => { + expect(() => { parseRange(''); - }).to.throwException(TypeError); + }).toThrowError(TypeError); - expect(function () { + expect(function() { parseRange('p10202'); - }).to.throwException(TypeError); + }).toThrowError(TypeError); - expect(function () { + expect(function() { parseRange('{0,100}'); - }).to.throwException(TypeError); + }).toThrowError(TypeError); - expect(function () { + expect(function() { parseRange('[0,100'); - }).to.throwException(TypeError); + }).toThrowError(TypeError); - expect(function () { + expect(function() { parseRange(')0,100('); - }).to.throwException(TypeError); + }).toThrowError(TypeError); }); const tests = { @@ -51,52 +49,52 @@ describe('Range parsing utility', function () { min: 0, max: 100, minInclusive: true, - maxInclusive: true + maxInclusive: true, }, within: [ [0, true], [0.0000001, true], [1, true], [99.99999, true], - [100, true] - ] + [100, true], + ], }, '(26.3 , 42]': { props: { min: 26.3, max: 42, minInclusive: false, - maxInclusive: true + maxInclusive: true, }, within: [ [26.2999999, false], [26.3000001, true], [30, true], [41, true], - [42, true] - ] + [42, true], + ], }, '(-50,50)': { props: { min: -50, max: 50, minInclusive: false, - maxInclusive: false + maxInclusive: false, }, within: [ [-50, false], [-49.99999, true], [0, true], [49.99999, true], - [50, false] - ] + [50, false], + ], }, '(Infinity, -Infinity)': { props: { min: -Infinity, max: Infinity, minInclusive: false, - maxInclusive: false + maxInclusive: false, }, within: [ [0, true], @@ -105,25 +103,24 @@ describe('Range parsing utility', function () { [-10000000000, true], [-Infinity, false], [Infinity, false], - ] - } + ], + }, }; - _.forOwn(tests, function (spec, str) { - - describe(str, function () { + forOwn(tests, (spec, str: any) => { + // eslint-disable-next-line jest/valid-describe + describe(str, () => { const range = parseRange(str); - it('creation', function () { - expect(range).to.eql(spec.props); + it('creation', () => { + expect(range).toEqual(spec.props); }); - spec.within.forEach(function (tup) { - it('#within(' + tup[0] + ')', function () { - expect(range.within(tup[0])).to.be(tup[1]); + spec.within.forEach((tup: any[]) => { + it('#within(' + tup[0] + ')', () => { + expect(range.within(tup[0])).toBe(tup[1]); }); }); }); - }); }); diff --git a/src/legacy/ui/public/utils/range.js b/src/legacy/ui/public/vis/editors/default/controls/components/number_list/range.ts similarity index 59% rename from src/legacy/ui/public/utils/range.js rename to src/legacy/ui/public/vis/editors/default/controls/components/number_list/range.ts index 54bd1b1903346..da3b7a61aea9d 100644 --- a/src/legacy/ui/public/utils/range.js +++ b/src/legacy/ui/public/vis/editors/default/controls/components/number_list/range.ts @@ -17,8 +17,6 @@ * under the License. */ -import _ from 'lodash'; - /** * Regexp portion that matches our number * @@ -44,41 +42,44 @@ const _RE_NUMBER = '(\\-?(?:\\d+(?:\\.\\d+)?|Infinity))'; * * @type {RegExp} */ -const RANGE_RE = new RegExp('^\\s*([\\[|\\(])\\s*' + _RE_NUMBER + '\\s*,\\s*' + _RE_NUMBER + '\\s*([\\]|\\)])\\s*$'); +const RANGE_RE = new RegExp( + '^\\s*([\\[|\\(])\\s*' + _RE_NUMBER + '\\s*,\\s*' + _RE_NUMBER + '\\s*([\\]|\\)])\\s*$' +); + +export class NumberListRange { + constructor( + public minInclusive: boolean, + public min: number, + public max: number, + public maxInclusive: boolean + ) {} -export function parseRange(input) { + within(n: number): boolean { + if ((this.min === n && !this.minInclusive) || this.min > n) return false; + if ((this.max === n && !this.maxInclusive) || this.max < n) return false; + + return true; + } +} +export function parseRange(input: string): NumberListRange { const match = String(input).match(RANGE_RE); if (!match) { throw new TypeError('expected input to be in interval notation e.g., (100, 200]'); } - return new Range( - match[1] === '[', - parseFloat(match[2]), - parseFloat(match[3]), - match[4] === ']' - ); -} - -function Range(/* minIncl, min, max, maxIncl */) { - const args = _.toArray(arguments); - if (args[1] > args[2]) args.reverse(); + const args = [match[1] === '[', parseFloat(match[2]), parseFloat(match[3]), match[4] === ']']; - this.minInclusive = args[0]; - this.min = args[1]; - this.max = args[2]; - this.maxInclusive = args[3]; -} - -Range.prototype.within = function (n) { - if (this.min === n && !this.minInclusive) return false; - if (this.min > n) return false; - - if (this.max === n && !this.maxInclusive) return false; - if (this.max < n) return false; - - return true; -}; + if (args[1] > args[2]) { + args.reverse(); + } + const [minInclusive, min, max, maxInclusive] = args; + return new NumberListRange( + minInclusive as boolean, + min as number, + max as number, + maxInclusive as boolean + ); +} diff --git a/src/legacy/ui/public/vis/editors/default/controls/components/number_list/utils.test.ts b/src/legacy/ui/public/vis/editors/default/controls/components/number_list/utils.test.ts index c6772cc108762..89fb5738db379 100644 --- a/src/legacy/ui/public/vis/editors/default/controls/components/number_list/utils.test.ts +++ b/src/legacy/ui/public/vis/editors/default/controls/components/number_list/utils.test.ts @@ -27,12 +27,12 @@ import { getNextModel, getRange, } from './utils'; -import { Range } from '../../../../../../utils/range'; +import { NumberListRange } from './range'; import { NumberRowModel } from './number_row'; describe('NumberList utils', () => { let modelList: NumberRowModel[]; - let range: Range; + let range: NumberListRange; beforeEach(() => { modelList = [ diff --git a/src/legacy/ui/public/vis/editors/default/controls/components/number_list/utils.ts b/src/legacy/ui/public/vis/editors/default/controls/components/number_list/utils.ts index 563e8f0a6a9b7..399253f27445c 100644 --- a/src/legacy/ui/public/vis/editors/default/controls/components/number_list/utils.ts +++ b/src/legacy/ui/public/vis/editors/default/controls/components/number_list/utils.ts @@ -21,7 +21,7 @@ import { last } from 'lodash'; import { i18n } from '@kbn/i18n'; import { htmlIdGenerator } from '@elastic/eui'; -import { parseRange, Range } from '../../../../../../utils/range'; +import { parseRange, NumberListRange } from './range'; import { NumberRowModel } from './number_row'; const EMPTY_STRING = ''; @@ -34,7 +34,7 @@ function parse(value: string) { return isNaN(parsedValue) ? EMPTY_STRING : parsedValue; } -function getRange(range?: string): Range { +function getRange(range?: string): NumberListRange { try { return range ? parseRange(range) : defaultRange; } catch (e) { @@ -42,7 +42,7 @@ function getRange(range?: string): Range { } } -function validateValue(value: number | '', numberRange: Range) { +function validateValue(value: number | '', numberRange: NumberListRange) { const result: { isInvalid: boolean; error?: string } = { isInvalid: false, }; @@ -76,7 +76,7 @@ function validateOrder(list: Array) { return result; } -function getNextModel(list: NumberRowModel[], range: Range): NumberRowModel { +function getNextModel(list: NumberRowModel[], range: NumberListRange): NumberRowModel { const lastValue = last(list).value; let next = Number(lastValue) ? Number(lastValue) + 1 : 1; @@ -104,7 +104,7 @@ function getInitModelList(list: Array): NumberRowModel[] { function getUpdatedModels( numberList: Array, modelList: NumberRowModel[], - numberRange: Range, + numberRange: NumberListRange, invalidOrderModelIndex?: number ): NumberRowModel[] { if (!numberList.length) { diff --git a/src/legacy/ui/public/vis/map/convert_to_geojson.js b/src/legacy/ui/public/vis/map/convert_to_geojson.js index 77896490678ff..14c282b58beda 100644 --- a/src/legacy/ui/public/vis/map/convert_to_geojson.js +++ b/src/legacy/ui/public/vis/map/convert_to_geojson.js @@ -17,10 +17,9 @@ * under the License. */ -import { decodeGeoHash } from 'ui/utils/decode_geo_hash'; +import { decodeGeoHash } from './decode_geo_hash'; import { gridDimensions } from './grid_dimensions'; - export function convertToGeoJson(tabifiedResponse, { geohash, geocentroid, metric }) { let features; diff --git a/src/legacy/ui/public/utils/__tests__/decode_geo_hash.test.js b/src/legacy/ui/public/vis/map/decode_geo_hash.test.ts similarity index 75% rename from src/legacy/ui/public/utils/__tests__/decode_geo_hash.test.js rename to src/legacy/ui/public/vis/map/decode_geo_hash.test.ts index 1ffe9ca7b4df2..c1ca7e4c80383 100644 --- a/src/legacy/ui/public/utils/__tests__/decode_geo_hash.test.js +++ b/src/legacy/ui/public/vis/map/decode_geo_hash.test.ts @@ -17,27 +17,18 @@ * under the License. */ -import { geohashColumns, decodeGeoHash } from '../decode_geo_hash'; +import { geohashColumns, decodeGeoHash } from './decode_geo_hash'; -test('geohashColumns', function () { +test('geohashColumns', () => { expect(geohashColumns(1)).toBe(8); expect(geohashColumns(2)).toBe(8 * 4); expect(geohashColumns(3)).toBe(8 * 4 * 8); expect(geohashColumns(4)).toBe(8 * 4 * 8 * 4); }); -test('decodeGeoHash', function () { +test('decodeGeoHash', () => { expect(decodeGeoHash('drm3btev3e86')).toEqual({ - latitude: [ - 41.119999922811985, - 41.12000009045005, - 41.12000000663102, - ], - longitude: [ - -71.34000029414892, - -71.3399999588728, - -71.34000012651086, - ], + latitude: [41.119999922811985, 41.12000009045005, 41.12000000663102], + longitude: [-71.34000029414892, -71.3399999588728, -71.34000012651086], }); }); - diff --git a/src/legacy/ui/public/utils/decode_geo_hash.ts b/src/legacy/ui/public/vis/map/decode_geo_hash.ts similarity index 100% rename from src/legacy/ui/public/utils/decode_geo_hash.ts rename to src/legacy/ui/public/vis/map/decode_geo_hash.ts diff --git a/src/legacy/ui/public/vis/map/kibana_map.js b/src/legacy/ui/public/vis/map/kibana_map.js index dc57809b6570f..cb618444af7ce 100644 --- a/src/legacy/ui/public/vis/map/kibana_map.js +++ b/src/legacy/ui/public/vis/map/kibana_map.js @@ -22,7 +22,7 @@ import { createZoomWarningMsg } from './map_messages'; import L from 'leaflet'; import $ from 'jquery'; import _ from 'lodash'; -import { zoomToPrecision } from '../../utils/zoom_to_precision'; +import { zoomToPrecision } from './zoom_to_precision'; import { i18n } from '@kbn/i18n'; import { ORIGIN } from '../../../../core_plugins/tile_map/common/origin'; diff --git a/src/legacy/ui/public/utils/zoom_to_precision.js b/src/legacy/ui/public/vis/map/zoom_to_precision.ts similarity index 52% rename from src/legacy/ui/public/utils/zoom_to_precision.js rename to src/legacy/ui/public/vis/map/zoom_to_precision.ts index f5c16b640d127..552c509590286 100644 --- a/src/legacy/ui/public/utils/zoom_to_precision.js +++ b/src/legacy/ui/public/vis/map/zoom_to_precision.ts @@ -19,39 +19,42 @@ import { geohashColumns } from './decode_geo_hash'; -const maxPrecision = 12; -/** - * Map Leaflet zoom levels to geohash precision levels. - * The size of a geohash column-width on the map should be at least `minGeohashPixels` pixels wide. - */ - - +const defaultMaxPrecision = 12; +const minGeoHashPixels = 16; - -const zoomPrecisionMap = {}; -const minGeohashPixels = 16; - -function calculateZoomToPrecisionMap(maxZoom) { +const calculateZoomToPrecisionMap = (maxZoom: number): Map => { + /** + * Map Leaflet zoom levels to geohash precision levels. + * The size of a geohash column-width on the map should be at least `minGeohashPixels` pixels wide. + */ + const zoomPrecisionMap = new Map(); for (let zoom = 0; zoom <= maxZoom; zoom += 1) { - if (typeof zoomPrecisionMap[zoom] === 'number') { + if (typeof zoomPrecisionMap.get(zoom) === 'number') { continue; } + const worldPixels = 256 * Math.pow(2, zoom); - zoomPrecisionMap[zoom] = 1; - for (let precision = 2; precision <= maxPrecision; precision += 1) { + + zoomPrecisionMap.set(zoom, 1); + + for (let precision = 2; precision <= defaultMaxPrecision; precision += 1) { const columns = geohashColumns(precision); - if ((worldPixels / columns) >= minGeohashPixels) { - zoomPrecisionMap[zoom] = precision; + + if (worldPixels / columns >= minGeoHashPixels) { + zoomPrecisionMap.set(zoom, precision); } else { break; } } } -} + return zoomPrecisionMap; +}; + +export function zoomToPrecision(mapZoom: number, maxPrecision: number, maxZoom: number) { + const zoomPrecisionMap = calculateZoomToPrecisionMap(typeof maxZoom === 'number' ? maxZoom : 21); + const precision = zoomPrecisionMap.get(mapZoom); -export function zoomToPrecision(mapZoom, maxPrecision, maxZoom) { - calculateZoomToPrecisionMap(typeof maxZoom === 'number' ? maxZoom : 21); - return Math.min(zoomPrecisionMap[mapZoom], maxPrecision); + return precision ? Math.min(precision, maxPrecision) : maxPrecision; } From 6a8b2a25c824d1d8eb12178f84a6a16ce92b594b Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Wed, 11 Dec 2019 10:30:45 +0300 Subject: [PATCH 15/40] [ui/public/utils] Delete unused base_object & find_by_param (#52500) Closes #51854 --- .../ui/public/state_management/state.js | 21 +++ .../ui/public/utils/__tests__/base_object.js | 57 ------ .../public/utils/__tests__/simple_emitter.js | 175 ------------------ src/legacy/ui/public/utils/base_object.ts | 47 ----- src/legacy/ui/public/utils/find_by_param.ts | 38 ---- src/legacy/ui/public/utils/simple_emitter.js | 4 - .../ui/public/utils/simple_emitter.test.js | 173 +++++++++++++++++ 7 files changed, 194 insertions(+), 321 deletions(-) delete mode 100644 src/legacy/ui/public/utils/__tests__/base_object.js delete mode 100644 src/legacy/ui/public/utils/__tests__/simple_emitter.js delete mode 100644 src/legacy/ui/public/utils/base_object.ts delete mode 100644 src/legacy/ui/public/utils/find_by_param.ts create mode 100644 src/legacy/ui/public/utils/simple_emitter.test.js diff --git a/src/legacy/ui/public/state_management/state.js b/src/legacy/ui/public/state_management/state.js index 27186b4249978..e868abb98c852 100644 --- a/src/legacy/ui/public/state_management/state.js +++ b/src/legacy/ui/public/state_management/state.js @@ -316,6 +316,27 @@ export function StateProvider(Private, $rootScope, $location, stateManagementCon return this._urlParam; }; + /** + * Returns an object with each property name and value corresponding to the entries in this collection + * excluding fields started from '$', '_' and all methods + * + * @return {object} + */ + State.prototype.toObject = function () { + return _.omit(this, (value, key) => { + return key.charAt(0) === '$' || key.charAt(0) === '_' || _.isFunction(value); + }); + }; + + /** Alias for method 'toObject' + * + * @obsolete Please use 'toObject' method instead + * @return {object} + */ + State.prototype.toJSON = function () { + return this.toObject(); + }; + return State; } diff --git a/src/legacy/ui/public/utils/__tests__/base_object.js b/src/legacy/ui/public/utils/__tests__/base_object.js deleted file mode 100644 index dfc5688c7b2f4..0000000000000 --- a/src/legacy/ui/public/utils/__tests__/base_object.js +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import expect from '@kbn/expect'; -import ngMock from 'ng_mock'; -import '../../private'; - -import { BaseObject } from '../base_object'; - -describe('Base Object', function () { - beforeEach(ngMock.module('kibana')); - - it('should take an inital set of values', function () { - const baseObject = new BaseObject({ message: 'test' }); - expect(baseObject).to.have.property('message', 'test'); - }); - - it('should serialize attributes to RISON', function () { - const baseObject = new BaseObject(); - baseObject.message = 'Testing... 1234'; - const rison = baseObject.toRISON(); - expect(rison).to.equal('(message:\'Testing... 1234\')'); - }); - - it('should not serialize $$attributes to RISON', function () { - const baseObject = new BaseObject(); - baseObject.$$attributes = { foo: 'bar' }; - baseObject.message = 'Testing... 1234'; - const rison = baseObject.toRISON(); - expect(rison).to.equal('(message:\'Testing... 1234\')'); - }); - - it('should serialize attributes for JSON', function () { - const baseObject = new BaseObject(); - baseObject.message = 'Testing... 1234'; - baseObject._private = 'foo'; - baseObject.$private = 'stuff'; - const json = JSON.stringify(baseObject); - expect(json).to.equal('{"message":"Testing... 1234"}'); - }); -}); diff --git a/src/legacy/ui/public/utils/__tests__/simple_emitter.js b/src/legacy/ui/public/utils/__tests__/simple_emitter.js deleted file mode 100644 index 25224a409f8f4..0000000000000 --- a/src/legacy/ui/public/utils/__tests__/simple_emitter.js +++ /dev/null @@ -1,175 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { SimpleEmitter } from '../simple_emitter'; -import expect from '@kbn/expect'; -import sinon from 'sinon'; - -describe('SimpleEmitter class', function () { - let emitter; - - beforeEach(function () { - emitter = new SimpleEmitter(); - }); - - it('constructs an event emitter', function () { - expect(emitter).to.have.property('on'); - expect(emitter).to.have.property('off'); - expect(emitter).to.have.property('emit'); - expect(emitter).to.have.property('listenerCount'); - expect(emitter).to.have.property('removeAllListeners'); - }); - - describe('#listenerCount', function () { - it('counts all event listeners without any arg', function () { - expect(emitter.listenerCount()).to.be(0); - emitter.on('a', function () {}); - expect(emitter.listenerCount()).to.be(1); - emitter.on('b', function () {}); - expect(emitter.listenerCount()).to.be(2); - }); - - it('limits to the event that is passed in', function () { - expect(emitter.listenerCount()).to.be(0); - emitter.on('a', function () {}); - expect(emitter.listenerCount('a')).to.be(1); - emitter.on('a', function () {}); - expect(emitter.listenerCount('a')).to.be(2); - emitter.on('b', function () {}); - expect(emitter.listenerCount('a')).to.be(2); - expect(emitter.listenerCount('b')).to.be(1); - expect(emitter.listenerCount()).to.be(3); - }); - }); - - describe('#on', function () { - it('registers a handler', function () { - const handler = sinon.stub(); - emitter.on('a', handler); - expect(emitter.listenerCount('a')).to.be(1); - - expect(handler.callCount).to.be(0); - emitter.emit('a'); - expect(handler.callCount).to.be(1); - }); - - it('allows multiple event handlers for the same event', function () { - emitter.on('a', function () {}); - emitter.on('a', function () {}); - expect(emitter.listenerCount('a')).to.be(2); - }); - - it('allows the same function to be registered multiple times', function () { - const handler = function () {}; - emitter.on('a', handler); - expect(emitter.listenerCount()).to.be(1); - emitter.on('a', handler); - expect(emitter.listenerCount()).to.be(2); - }); - }); - - describe('#off', function () { - it('removes a listener if it was registered', function () { - const handler = sinon.stub(); - expect(emitter.listenerCount()).to.be(0); - emitter.on('a', handler); - expect(emitter.listenerCount('a')).to.be(1); - emitter.off('a', handler); - expect(emitter.listenerCount('a')).to.be(0); - }); - - it('clears all listeners if no handler is passed', function () { - emitter.on('a', function () {}); - emitter.on('a', function () {}); - expect(emitter.listenerCount()).to.be(2); - emitter.off('a'); - expect(emitter.listenerCount()).to.be(0); - }); - - it('does not mind if the listener is not registered', function () { - emitter.off('a', function () {}); - }); - - it('does not mind if the event has no listeners', function () { - emitter.off('a'); - }); - }); - - describe('#emit', function () { - it('calls the handlers in the order they were defined', function () { - let i = 0; - const incr = function () { return ++i; }; - const one = sinon.spy(incr); - const two = sinon.spy(incr); - const three = sinon.spy(incr); - const four = sinon.spy(incr); - - emitter - .on('a', one) - .on('a', two) - .on('a', three) - .on('a', four) - .emit('a'); - - expect(one).to.have.property('callCount', 1); - expect(one.returned(1)).to.be.ok(); - - expect(two).to.have.property('callCount', 1); - expect(two.returned(2)).to.be.ok(); - - expect(three).to.have.property('callCount', 1); - expect(three.returned(3)).to.be.ok(); - - expect(four).to.have.property('callCount', 1); - expect(four.returned(4)).to.be.ok(); - }); - - it('always emits the handlers that were initially registered', function () { - - const destructive = sinon.spy(function () { - emitter.removeAllListeners(); - expect(emitter.listenerCount()).to.be(0); - }); - const stub = sinon.stub(); - - emitter.on('run', destructive).on('run', stub).emit('run'); - - expect(destructive).to.have.property('callCount', 1); - expect(stub).to.have.property('callCount', 1); - }); - - it('applies all arguments except the first', function () { - emitter - .on('a', function (a, b, c) { - expect(a).to.be('foo'); - expect(b).to.be('bar'); - expect(c).to.be('baz'); - }) - .emit('a', 'foo', 'bar', 'baz'); - }); - - it('uses the SimpleEmitter as the this context', function () { - emitter - .on('a', function () { - expect(this).to.be(emitter); - }) - .emit('a'); - }); - }); -}); diff --git a/src/legacy/ui/public/utils/base_object.ts b/src/legacy/ui/public/utils/base_object.ts deleted file mode 100644 index 63c7ebf6de5bb..0000000000000 --- a/src/legacy/ui/public/utils/base_object.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import angular from 'angular'; -import _ from 'lodash'; -// @ts-ignore -- awaiting https://github.com/w33ble/rison-node/issues/1 -import rison from 'rison-node'; - -export class BaseObject { - // Set the attributes or default to an empty object - constructor(attributes: Record = {}) { - // Set the attributes or default to an empty object - _.assign(this, attributes); - } - - public toObject() { - // return just the data. - return _.omit(this, (value: any, key: string) => { - return key.charAt(0) === '$' || key.charAt(0) === '_' || _.isFunction(value); - }); - } - - public toRISON() { - // Use Angular to remove the private vars, and JSON.stringify to serialize - return rison.encode(JSON.parse(angular.toJson(this))); - } - - public toJSON() { - return this.toObject(); - } -} diff --git a/src/legacy/ui/public/utils/find_by_param.ts b/src/legacy/ui/public/utils/find_by_param.ts deleted file mode 100644 index de32fc955a8cd..0000000000000 --- a/src/legacy/ui/public/utils/find_by_param.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import _ from 'lodash'; - -interface AnyObject { - [key: string]: any; -} - -// given an object or array of objects, return the value of the passed param -// if the param is missing, return undefined -export function findByParam(values: AnyObject | AnyObject[], param: string) { - if (Array.isArray(values)) { - // point series chart - const index = _.findIndex(values, param); - if (index === -1) { - return; - } - return values[index][param]; - } - return values[param]; // pie chart -} diff --git a/src/legacy/ui/public/utils/simple_emitter.js b/src/legacy/ui/public/utils/simple_emitter.js index 84397962c286b..503798ba160db 100644 --- a/src/legacy/ui/public/utils/simple_emitter.js +++ b/src/legacy/ui/public/utils/simple_emitter.js @@ -18,8 +18,6 @@ */ import _ from 'lodash'; -import { BaseObject } from './base_object'; -import { createLegacyClass } from './legacy_class'; /** * Simple event emitter class used in the vislib. Calls @@ -27,7 +25,6 @@ import { createLegacyClass } from './legacy_class'; * * @class */ -createLegacyClass(SimpleEmitter).inherits(BaseObject); export function SimpleEmitter() { this._listeners = {}; } @@ -134,4 +131,3 @@ SimpleEmitter.prototype.listenerCount = function (name) { return count + _.size(handlers); }, 0); }; - diff --git a/src/legacy/ui/public/utils/simple_emitter.test.js b/src/legacy/ui/public/utils/simple_emitter.test.js new file mode 100644 index 0000000000000..ff884a12be7ee --- /dev/null +++ b/src/legacy/ui/public/utils/simple_emitter.test.js @@ -0,0 +1,173 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { SimpleEmitter } from './simple_emitter'; +import sinon from 'sinon'; + +describe('SimpleEmitter class', () => { + let emitter; + + beforeEach(() => { + emitter = new SimpleEmitter(); + }); + + it('constructs an event emitter', () => { + expect(emitter).toHaveProperty('on'); + expect(emitter).toHaveProperty('off'); + expect(emitter).toHaveProperty('emit'); + expect(emitter).toHaveProperty('listenerCount'); + expect(emitter).toHaveProperty('removeAllListeners'); + }); + + describe('#listenerCount', () => { + it('counts all event listeners without any arg', () => { + expect(emitter.listenerCount()).toBe(0); + emitter.on('a', () => {}); + expect(emitter.listenerCount()).toBe(1); + emitter.on('b', () => {}); + expect(emitter.listenerCount()).toBe(2); + }); + + it('limits to the event that is passed in', () => { + expect(emitter.listenerCount()).toBe(0); + emitter.on('a', () => {}); + expect(emitter.listenerCount('a')).toBe(1); + emitter.on('a', () => {}); + expect(emitter.listenerCount('a')).toBe(2); + emitter.on('b', () => {}); + expect(emitter.listenerCount('a')).toBe(2); + expect(emitter.listenerCount('b')).toBe(1); + expect(emitter.listenerCount()).toBe(3); + }); + }); + + describe('#on', () => { + it('registers a handler', () => { + const handler = sinon.stub(); + emitter.on('a', handler); + expect(emitter.listenerCount('a')).toBe(1); + + expect(handler.callCount).toBe(0); + emitter.emit('a'); + expect(handler.callCount).toBe(1); + }); + + it('allows multiple event handlers for the same event', () => { + emitter.on('a', () => {}); + emitter.on('a', () => {}); + expect(emitter.listenerCount('a')).toBe(2); + }); + + it('allows the same function to be registered multiple times', () => { + const handler = () => {}; + emitter.on('a', handler); + expect(emitter.listenerCount()).toBe(1); + emitter.on('a', handler); + expect(emitter.listenerCount()).toBe(2); + }); + }); + + describe('#off', () => { + it('removes a listener if it was registered', () => { + const handler = sinon.stub(); + expect(emitter.listenerCount()).toBe(0); + emitter.on('a', handler); + expect(emitter.listenerCount('a')).toBe(1); + emitter.off('a', handler); + expect(emitter.listenerCount('a')).toBe(0); + }); + + it('clears all listeners if no handler is passed', () => { + emitter.on('a', () => {}); + emitter.on('a', () => {}); + expect(emitter.listenerCount()).toBe(2); + emitter.off('a'); + expect(emitter.listenerCount()).toBe(0); + }); + + it('does not mind if the listener is not registered', () => { + emitter.off('a', () => {}); + }); + + it('does not mind if the event has no listeners', () => { + emitter.off('a'); + }); + }); + + describe('#emit', () => { + it('calls the handlers in the order they were defined', () => { + let i = 0; + const incr = () => ++i; + const one = sinon.spy(incr); + const two = sinon.spy(incr); + const three = sinon.spy(incr); + const four = sinon.spy(incr); + + emitter + .on('a', one) + .on('a', two) + .on('a', three) + .on('a', four) + .emit('a'); + + expect(one).toHaveProperty('callCount', 1); + expect(one.returned(1)).toBeDefined(); + + expect(two).toHaveProperty('callCount', 1); + expect(two.returned(2)).toBeDefined(); + + expect(three).toHaveProperty('callCount', 1); + expect(three.returned(3)).toBeDefined(); + + expect(four).toHaveProperty('callCount', 1); + expect(four.returned(4)).toBeDefined(); + }); + + it('always emits the handlers that were initially registered', () => { + const destructive = sinon.spy(() => { + emitter.removeAllListeners(); + expect(emitter.listenerCount()).toBe(0); + }); + const stub = sinon.stub(); + + emitter.on('run', destructive).on('run', stub).emit('run'); + + expect(destructive).toHaveProperty('callCount', 1); + expect(stub).toHaveProperty('callCount', 1); + }); + + it('applies all arguments except the first', () => { + emitter + .on('a', (a, b, c) => { + expect(a).toBe('foo'); + expect(b).toBe('bar'); + expect(c).toBe('baz'); + }) + .emit('a', 'foo', 'bar', 'baz'); + }); + + it('uses the SimpleEmitter as the this context', () => { + emitter + .on('a', function () { + expect(this).toBe(emitter); + }) + .emit('a'); + }); + }); +}); From f0eb4bb675e23bada8f170097e12260bb00803cc Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Wed, 11 Dec 2019 08:47:44 +0100 Subject: [PATCH 16/40] [APM] Fix some warnings logged in APM tests (#52487) * [APM] Fix some warnings logged in APM tests (Seemingly) since the React upgrade in 439708a6f9, our tests have started logging various warnings/errors to the console. The test suite is still passing but it creates a lot of noise. Changes: - use `act` or `wait` when appropriate - mock useFetcher calls - cleanup in useDelayedVisbility * Replace tick() with wait() --- .../__jest__/TransactionOverview.test.tsx | 3 ++ .../DatePicker/__test__/DatePicker.test.tsx | 6 +-- .../useDelayedVisibility/Delayed/index.ts | 6 +++ .../useDelayedVisibility/index.test.tsx | 41 +++++++++++++++---- .../shared/useDelayedVisibility/index.ts | 4 ++ .../__tests__/UrlParamsContext.test.tsx | 10 ++--- .../hooks/useFetcher.integration.test.tsx | 13 +++--- .../plugins/apm/public/utils/testHelpers.tsx | 4 -- 8 files changed, 61 insertions(+), 26 deletions(-) diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/__jest__/TransactionOverview.test.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/__jest__/TransactionOverview.test.tsx index a5356be72f5e4..91e0ae11a652e 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/__jest__/TransactionOverview.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/__jest__/TransactionOverview.test.tsx @@ -18,6 +18,7 @@ import { history } from '../../../../utils/history'; import { TransactionOverview } from '..'; import { IUrlParams } from '../../../../context/UrlParamsContext/types'; import * as useServiceTransactionTypesHook from '../../../../hooks/useServiceTransactionTypes'; +import * as useFetcherHook from '../../../../hooks/useFetcher'; import { fromQuery } from '../../../shared/Links/url_helpers'; import { Router } from 'react-router-dom'; import { UrlParamsProvider } from '../../../../context/UrlParamsContext'; @@ -51,6 +52,8 @@ function setup({ .spyOn(useServiceTransactionTypesHook, 'useServiceTransactionTypes') .mockReturnValue(serviceTransactionTypes); + jest.spyOn(useFetcherHook, 'useFetcher').mockReturnValue({} as any); + return render( diff --git a/x-pack/legacy/plugins/apm/public/components/shared/DatePicker/__test__/DatePicker.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/DatePicker/__test__/DatePicker.test.tsx index 05094c59712a9..32379325c4020 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/DatePicker/__test__/DatePicker.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/DatePicker/__test__/DatePicker.test.tsx @@ -10,13 +10,13 @@ import { UrlParamsContext, useUiFilters } from '../../../../context/UrlParamsContext'; -import { tick } from '../../../../utils/testHelpers'; import { DatePicker } from '../index'; import { IUrlParams } from '../../../../context/UrlParamsContext/types'; import { history } from '../../../../utils/history'; import { mount } from 'enzyme'; import { EuiSuperDatePicker } from '@elastic/eui'; import { MemoryRouter } from 'react-router-dom'; +import { wait } from '@testing-library/react'; const mockHistoryPush = jest.spyOn(history, 'push'); const mockRefreshTimeRange = jest.fn(); @@ -84,7 +84,7 @@ describe('DatePicker', () => { }); expect(mockRefreshTimeRange).not.toHaveBeenCalled(); jest.advanceTimersByTime(1000); - await tick(); + await wait(); expect(mockRefreshTimeRange).toHaveBeenCalled(); wrapper.unmount(); }); @@ -94,7 +94,7 @@ describe('DatePicker', () => { mountDatePicker({ refreshPaused: true, refreshInterval: 1000 }); expect(mockRefreshTimeRange).not.toHaveBeenCalled(); jest.advanceTimersByTime(1000); - await tick(); + await wait(); expect(mockRefreshTimeRange).not.toHaveBeenCalled(); }); }); diff --git a/x-pack/legacy/plugins/apm/public/components/shared/useDelayedVisibility/Delayed/index.ts b/x-pack/legacy/plugins/apm/public/components/shared/useDelayedVisibility/Delayed/index.ts index 798e872dbc472..9048afe57153d 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/useDelayedVisibility/Delayed/index.ts +++ b/x-pack/legacy/plugins/apm/public/components/shared/useDelayedVisibility/Delayed/index.ts @@ -57,4 +57,10 @@ export class Delayed { public onChange(onChangeCallback: Callback) { this.onChangeCallback = onChangeCallback; } + + public destroy() { + if (this.timeoutId) { + window.clearTimeout(this.timeoutId); + } + } } diff --git a/x-pack/legacy/plugins/apm/public/components/shared/useDelayedVisibility/index.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/useDelayedVisibility/index.test.tsx index 57e634df22837..c55c6ab351848 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/useDelayedVisibility/index.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/useDelayedVisibility/index.test.tsx @@ -4,11 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { renderHook } from '@testing-library/react-hooks'; +import { + renderHook, + act, + RenderHookResult +} from '@testing-library/react-hooks'; import { useDelayedVisibility } from '.'; describe('useFetcher', () => { - let hook; + let hook: RenderHookResult; + beforeEach(() => { jest.useFakeTimers(); }); @@ -26,9 +31,15 @@ describe('useFetcher', () => { }); hook.rerender(true); - jest.advanceTimersByTime(10); + act(() => { + jest.advanceTimersByTime(10); + }); + expect(hook.result.current).toEqual(false); - jest.advanceTimersByTime(50); + act(() => { + jest.advanceTimersByTime(50); + }); + expect(hook.result.current).toEqual(true); }); @@ -38,8 +49,11 @@ describe('useFetcher', () => { }); hook.rerender(true); - jest.advanceTimersByTime(100); + act(() => { + jest.advanceTimersByTime(100); + }); hook.rerender(false); + expect(hook.result.current).toEqual(true); }); @@ -49,11 +63,22 @@ describe('useFetcher', () => { }); hook.rerender(true); - jest.advanceTimersByTime(100); + + act(() => { + jest.advanceTimersByTime(100); + }); + hook.rerender(false); - jest.advanceTimersByTime(900); + act(() => { + jest.advanceTimersByTime(900); + }); + expect(hook.result.current).toEqual(true); - jest.advanceTimersByTime(100); + + act(() => { + jest.advanceTimersByTime(100); + }); + expect(hook.result.current).toEqual(false); }); }); diff --git a/x-pack/legacy/plugins/apm/public/components/shared/useDelayedVisibility/index.ts b/x-pack/legacy/plugins/apm/public/components/shared/useDelayedVisibility/index.ts index 5acbbd1d45737..c4465c7b42339 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/useDelayedVisibility/index.ts +++ b/x-pack/legacy/plugins/apm/public/components/shared/useDelayedVisibility/index.ts @@ -26,6 +26,10 @@ export function useDelayedVisibility( setIsVisible(visibility); }); delayedRef.current = delayed; + + return () => { + delayed.destroy(); + }; }, [hideDelayMs, showDelayMs, minimumVisibleDuration]); useEffect(() => { diff --git a/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/__tests__/UrlParamsContext.test.tsx b/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/__tests__/UrlParamsContext.test.tsx index 2604a3a122574..d2d8036e864ae 100644 --- a/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/__tests__/UrlParamsContext.test.tsx +++ b/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/__tests__/UrlParamsContext.test.tsx @@ -11,8 +11,8 @@ import { Location, History } from 'history'; import { MemoryRouter, Router } from 'react-router-dom'; import moment from 'moment-timezone'; import { IUrlParams } from '../types'; -import { tick } from '../../../utils/testHelpers'; import { getParsedDate } from '../helpers'; +import { wait } from '@testing-library/react'; function mountParams(location: Location) { return mount( @@ -143,13 +143,13 @@ describe('UrlParamsContext', () => { ); - await tick(); + await wait(); expect(calls.length).toBe(1); wrapper.find('button').simulate('click'); - await tick(); + await wait(); expect(calls.length).toBe(2); @@ -194,11 +194,11 @@ describe('UrlParamsContext', () => { ); - await tick(); + await wait(); wrapper.find('button').simulate('click'); - await tick(); + await wait(); const params = getDataFromOutput(wrapper); expect(params.start).toEqual('2000-06-14T00:00:00.000Z'); diff --git a/x-pack/legacy/plugins/apm/public/hooks/useFetcher.integration.test.tsx b/x-pack/legacy/plugins/apm/public/hooks/useFetcher.integration.test.tsx index 36a8377c02527..743cf4e01e555 100644 --- a/x-pack/legacy/plugins/apm/public/hooks/useFetcher.integration.test.tsx +++ b/x-pack/legacy/plugins/apm/public/hooks/useFetcher.integration.test.tsx @@ -5,8 +5,8 @@ */ import React from 'react'; -import { render } from '@testing-library/react'; -import { delay, tick } from '../utils/testHelpers'; +import { render, wait } from '@testing-library/react'; +import { delay } from '../utils/testHelpers'; import { useFetcher } from './useFetcher'; import { KibanaCoreContext } from '../../../observability/public/context/kibana_core'; import { LegacyCoreStart } from 'kibana/public'; @@ -76,7 +76,8 @@ describe('when simulating race condition', () => { it('should render "Hello from Peter" after 200ms', async () => { jest.advanceTimersByTime(200); - await tick(); + + await wait(); expect(renderSpy).lastCalledWith({ data: 'Hello from Peter', @@ -87,7 +88,7 @@ describe('when simulating race condition', () => { it('should render "Hello from Peter" after 600ms', async () => { jest.advanceTimersByTime(600); - await tick(); + await wait(); expect(renderSpy).lastCalledWith({ data: 'Hello from Peter', @@ -98,7 +99,7 @@ describe('when simulating race condition', () => { it('should should NOT have rendered "Hello from John" at any point', async () => { jest.advanceTimersByTime(600); - await tick(); + await wait(); expect(renderSpy).not.toHaveBeenCalledWith({ data: 'Hello from John', @@ -109,7 +110,7 @@ describe('when simulating race condition', () => { it('should send and receive calls in the right order', async () => { jest.advanceTimersByTime(600); - await tick(); + await wait(); expect(requestCallOrder).toEqual([ ['request', 'John', 500], diff --git a/x-pack/legacy/plugins/apm/public/utils/testHelpers.tsx b/x-pack/legacy/plugins/apm/public/utils/testHelpers.tsx index b5cee4a78b01c..9e3c486715a1f 100644 --- a/x-pack/legacy/plugins/apm/public/utils/testHelpers.tsx +++ b/x-pack/legacy/plugins/apm/public/utils/testHelpers.tsx @@ -58,7 +58,6 @@ export async function getRenderedHref(Component: React.FC, location: Location) { ); - await tick(); await waitForElement(() => el.container.querySelector('a')); const a = el.container.querySelector('a'); @@ -74,9 +73,6 @@ export function delay(ms: number) { return new Promise(resolve => setTimeout(resolve, ms)); } -// Await this when you need to "flush" promises to immediately resolve or throw in tests -export const tick = () => new Promise(resolve => setImmediate(resolve, 0)); - export function expectTextsNotInDocument(output: any, texts: string[]) { texts.forEach(text => { try { From 7e27f0d35f87966499ba9f717d9c5068efe51174 Mon Sep 17 00:00:00 2001 From: Aleh Zasypkin Date: Wed, 11 Dec 2019 08:55:46 +0100 Subject: [PATCH 17/40] Decouple Authorization subsystem from Legacy API. (#52638) --- x-pack/legacy/plugins/security/index.js | 1 - .../authorization/check_privileges.test.ts | 6 +++--- .../server/authorization/check_privileges.ts | 11 ++++++----- .../security/server/authorization/index.mock.ts | 7 +++++-- .../security/server/authorization/index.test.ts | 7 +++---- .../security/server/authorization/index.ts | 16 ++++++++-------- x-pack/plugins/security/server/plugin.ts | 10 ++++++---- .../routes/authorization/roles/get.test.ts | 2 +- .../server/routes/authorization/roles/get.ts | 2 +- .../routes/authorization/roles/get_all.test.ts | 2 +- .../server/routes/authorization/roles/get_all.ts | 6 +----- .../routes/authorization/roles/put.test.ts | 2 +- .../server/routes/authorization/roles/put.ts | 2 +- 13 files changed, 37 insertions(+), 37 deletions(-) diff --git a/x-pack/legacy/plugins/security/index.js b/x-pack/legacy/plugins/security/index.js index 1d798a4a2bc40..115dd8b9b8206 100644 --- a/x-pack/legacy/plugins/security/index.js +++ b/x-pack/legacy/plugins/security/index.js @@ -132,7 +132,6 @@ export const security = (kibana) => new kibana.Plugin({ server.plugins.kibana.systemApi ), cspRules: createCSPRuleString(config.get('csp.rules')), - kibanaIndexName: config.get('kibana.index'), }); // Legacy xPack Info endpoint returns whatever we return in a callback for `registerLicenseCheckResultsGenerator` diff --git a/x-pack/plugins/security/server/authorization/check_privileges.test.ts b/x-pack/plugins/security/server/authorization/check_privileges.test.ts index b1cb78008da00..8c1241937892e 100644 --- a/x-pack/plugins/security/server/authorization/check_privileges.test.ts +++ b/x-pack/plugins/security/server/authorization/check_privileges.test.ts @@ -48,7 +48,7 @@ describe('#atSpace', () => { const checkPrivilegesWithRequest = checkPrivilegesWithRequestFactory( mockActions, mockClusterClient, - () => application + application ); const request = httpServerMock.createKibanaRequest(); const checkPrivileges = checkPrivilegesWithRequest(request); @@ -291,7 +291,7 @@ describe('#atSpaces', () => { const checkPrivilegesWithRequest = checkPrivilegesWithRequestFactory( mockActions, mockClusterClient, - () => application + application ); const request = httpServerMock.createKibanaRequest(); const checkPrivileges = checkPrivilegesWithRequest(request); @@ -772,7 +772,7 @@ describe('#globally', () => { const checkPrivilegesWithRequest = checkPrivilegesWithRequestFactory( mockActions, mockClusterClient, - () => application + application ); const request = httpServerMock.createKibanaRequest(); const checkPrivileges = checkPrivilegesWithRequest(request); diff --git a/x-pack/plugins/security/server/authorization/check_privileges.ts b/x-pack/plugins/security/server/authorization/check_privileges.ts index 5bc3ce075452d..3ef7a8f29a0bf 100644 --- a/x-pack/plugins/security/server/authorization/check_privileges.ts +++ b/x-pack/plugins/security/server/authorization/check_privileges.ts @@ -61,7 +61,7 @@ export interface CheckPrivileges { export function checkPrivilegesWithRequestFactory( actions: CheckPrivilegesActions, clusterClient: IClusterClient, - getApplicationName: () => string + applicationName: string ) { const hasIncompatibleVersion = ( applicationPrivilegesResponse: HasPrivilegesResponseApplication @@ -81,23 +81,24 @@ export function checkPrivilegesWithRequestFactory( : [privilegeOrPrivileges]; const allApplicationPrivileges = uniq([actions.version, actions.login, ...privileges]); - const application = getApplicationName(); const hasPrivilegesResponse = (await clusterClient .asScoped(request) .callAsCurrentUser('shield.hasPrivileges', { body: { - applications: [{ application, resources, privileges: allApplicationPrivileges }], + applications: [ + { application: applicationName, resources, privileges: allApplicationPrivileges }, + ], }, })) as HasPrivilegesResponse; validateEsPrivilegeResponse( hasPrivilegesResponse, - application, + applicationName, allApplicationPrivileges, resources ); - const applicationPrivilegesResponse = hasPrivilegesResponse.application[application]; + const applicationPrivilegesResponse = hasPrivilegesResponse.application[applicationName]; if (hasIncompatibleVersion(applicationPrivilegesResponse)) { throw new Error( diff --git a/x-pack/plugins/security/server/authorization/index.mock.ts b/x-pack/plugins/security/server/authorization/index.mock.ts index 2e700745c69dc..930ede4157723 100644 --- a/x-pack/plugins/security/server/authorization/index.mock.ts +++ b/x-pack/plugins/security/server/authorization/index.mock.ts @@ -8,12 +8,15 @@ import { Actions } from '.'; import { AuthorizationMode } from './mode'; export const authorizationMock = { - create: ({ version = 'mock-version' }: { version?: string } = {}) => ({ + create: ({ + version = 'mock-version', + applicationName = 'mock-application', + }: { version?: string; applicationName?: string } = {}) => ({ actions: new Actions(version), checkPrivilegesWithRequest: jest.fn(), checkPrivilegesDynamicallyWithRequest: jest.fn(), checkSavedObjectsPrivilegesWithRequest: jest.fn(), - getApplicationName: jest.fn().mockReturnValue('mock-application'), + applicationName, mode: { useRbacForRequest: jest.fn() } as jest.Mocked, privileges: { get: jest.fn() }, registerPrivilegesWithCluster: jest.fn(), diff --git a/x-pack/plugins/security/server/authorization/index.test.ts b/x-pack/plugins/security/server/authorization/index.test.ts index 24179e062230a..34b9efea77165 100644 --- a/x-pack/plugins/security/server/authorization/index.test.ts +++ b/x-pack/plugins/security/server/authorization/index.test.ts @@ -53,7 +53,6 @@ test(`returns exposed services`, () => { .fn() .mockReturnValue({ getSpaceId: jest.fn(), namespaceToSpaceId: jest.fn() }); const mockFeaturesService = { getFeatures: () => [] }; - const mockGetLegacyAPI = () => ({ kibanaIndexName }); const mockLicense = licenseMock.create(); const authz = setupAuthorization({ @@ -61,20 +60,20 @@ test(`returns exposed services`, () => { clusterClient: mockClusterClient, license: mockLicense, loggers: loggingServiceMock.create(), - getLegacyAPI: mockGetLegacyAPI, + kibanaIndexName, packageVersion: 'some-version', featuresService: mockFeaturesService, getSpacesService: mockGetSpacesService, }); expect(authz.actions.version).toBe('version:some-version'); - expect(authz.getApplicationName()).toBe(application); + expect(authz.applicationName).toBe(application); expect(authz.checkPrivilegesWithRequest).toBe(mockCheckPrivilegesWithRequest); expect(checkPrivilegesWithRequestFactory).toHaveBeenCalledWith( authz.actions, mockClusterClient, - authz.getApplicationName + authz.applicationName ); expect(authz.checkPrivilegesDynamicallyWithRequest).toBe( diff --git a/x-pack/plugins/security/server/authorization/index.ts b/x-pack/plugins/security/server/authorization/index.ts index b5f9efadbd8d0..41e6d12eb8f36 100644 --- a/x-pack/plugins/security/server/authorization/index.ts +++ b/x-pack/plugins/security/server/authorization/index.ts @@ -12,7 +12,7 @@ import { IClusterClient, } from '../../../../../src/core/server'; -import { FeaturesService, LegacyAPI, SpacesService } from '../plugin'; +import { FeaturesService, SpacesService } from '../plugin'; import { Actions } from './actions'; import { CheckPrivilegesWithRequest, checkPrivilegesWithRequestFactory } from './check_privileges'; import { @@ -43,7 +43,7 @@ interface SetupAuthorizationParams { license: SecurityLicense; loggers: LoggerFactory; featuresService: FeaturesService; - getLegacyAPI(): Pick; + kibanaIndexName: string; getSpacesService(): SpacesService | undefined; } @@ -52,7 +52,7 @@ export interface Authorization { checkPrivilegesWithRequest: CheckPrivilegesWithRequest; checkPrivilegesDynamicallyWithRequest: CheckPrivilegesDynamicallyWithRequest; checkSavedObjectsPrivilegesWithRequest: CheckSavedObjectsPrivilegesWithRequest; - getApplicationName: () => string; + applicationName: string; mode: AuthorizationMode; privileges: PrivilegesService; disableUnauthorizedCapabilities: ( @@ -69,23 +69,23 @@ export function setupAuthorization({ license, loggers, featuresService, - getLegacyAPI, + kibanaIndexName, getSpacesService, }: SetupAuthorizationParams): Authorization { const actions = new Actions(packageVersion); const mode = authorizationModeFactory(license); - const getApplicationName = () => `${APPLICATION_PREFIX}${getLegacyAPI().kibanaIndexName}`; + const applicationName = `${APPLICATION_PREFIX}${kibanaIndexName}`; const checkPrivilegesWithRequest = checkPrivilegesWithRequestFactory( actions, clusterClient, - getApplicationName + applicationName ); const privileges = privilegesFactory(actions, featuresService); const logger = loggers.get('authorization'); const authz = { actions, - getApplicationName, + applicationName, checkPrivilegesWithRequest, checkPrivilegesDynamicallyWithRequest: checkPrivilegesDynamicallyWithRequestFactory( checkPrivilegesWithRequest, @@ -123,7 +123,7 @@ export function setupAuthorization({ registerPrivilegesWithCluster: async () => { validateFeaturePrivileges(actions, featuresService.getFeatures()); - await registerPrivilegesWithCluster(logger, privileges, getApplicationName(), clusterClient); + await registerPrivilegesWithCluster(logger, privileges, applicationName, clusterClient); }, }; diff --git a/x-pack/plugins/security/server/plugin.ts b/x-pack/plugins/security/server/plugin.ts index cb197ecaf7e10..84d448331cef2 100644 --- a/x-pack/plugins/security/server/plugin.ts +++ b/x-pack/plugins/security/server/plugin.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Subscription } from 'rxjs'; +import { Subscription, combineLatest } from 'rxjs'; import { first } from 'rxjs/operators'; import { IClusterClient, @@ -42,7 +42,6 @@ export type FeaturesService = Pick; */ export interface LegacyAPI { isSystemAPIRequest: (request: KibanaRequest) => boolean; - kibanaIndexName: string; cspRules: string; savedObjects: SavedObjectsLegacyService; auditLogger: { @@ -121,7 +120,10 @@ export class Plugin { core: CoreSetup, { features, licensing }: PluginSetupDependencies ): Promise> { - const config = await createConfig$(this.initializerContext, core.http.isTlsEnabled) + const [config, legacyConfig] = await combineLatest([ + createConfig$(this.initializerContext, core.http.isTlsEnabled), + this.initializerContext.config.legacy.globalConfig$, + ]) .pipe(first()) .toPromise(); @@ -148,7 +150,7 @@ export class Plugin { clusterClient: this.clusterClient, license, loggers: this.initializerContext.logger, - getLegacyAPI: this.getLegacyAPI, + kibanaIndexName: legacyConfig.kibana.index, packageVersion: this.initializerContext.env.packageInfo.version, getSpacesService: this.getSpacesService, featuresService: features, diff --git a/x-pack/plugins/security/server/routes/authorization/roles/get.test.ts b/x-pack/plugins/security/server/routes/authorization/roles/get.test.ts index 1cfc1ae416ae4..447d890605cc9 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/get.test.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/get.test.ts @@ -36,7 +36,7 @@ describe('GET role', () => { ) => { test(description, async () => { const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); - mockRouteDefinitionParams.authz.getApplicationName.mockReturnValue(application); + mockRouteDefinitionParams.authz.applicationName = application; const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(mockScopedClusterClient); diff --git a/x-pack/plugins/security/server/routes/authorization/roles/get.ts b/x-pack/plugins/security/server/routes/authorization/roles/get.ts index be69e222dd093..1173d37cba64f 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/get.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/get.ts @@ -28,7 +28,7 @@ export function defineGetRolesRoutes({ router, authz, clusterClient }: RouteDefi body: transformElasticsearchRoleToRole( elasticsearchRole, request.params.name, - authz.getApplicationName() + authz.applicationName ), }); } diff --git a/x-pack/plugins/security/server/routes/authorization/roles/get_all.test.ts b/x-pack/plugins/security/server/routes/authorization/roles/get_all.test.ts index 76ce6a272e285..3bd85122c95d1 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/get_all.test.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/get_all.test.ts @@ -31,7 +31,7 @@ describe('GET all roles', () => { ) => { test(description, async () => { const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); - mockRouteDefinitionParams.authz.getApplicationName.mockReturnValue(application); + mockRouteDefinitionParams.authz.applicationName = application; const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(mockScopedClusterClient); diff --git a/x-pack/plugins/security/server/routes/authorization/roles/get_all.ts b/x-pack/plugins/security/server/routes/authorization/roles/get_all.ts index f5d2d51280fc4..6ff431f0f8b6a 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/get_all.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/get_all.ts @@ -22,11 +22,7 @@ export function defineGetAllRolesRoutes({ router, authz, clusterClient }: RouteD return response.ok({ body: Object.entries(elasticsearchRoles) .map(([roleName, elasticsearchRole]) => - transformElasticsearchRoleToRole( - elasticsearchRole, - roleName, - authz.getApplicationName() - ) + transformElasticsearchRoleToRole(elasticsearchRole, roleName, authz.applicationName) ) .sort((roleA, roleB) => { if (roleA.name < roleB.name) { diff --git a/x-pack/plugins/security/server/routes/authorization/roles/put.test.ts b/x-pack/plugins/security/server/routes/authorization/roles/put.test.ts index 31963987c2efb..cb80549df8417 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/put.test.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/put.test.ts @@ -62,7 +62,7 @@ const putRoleTest = ( ) => { test(description, async () => { const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); - mockRouteDefinitionParams.authz.getApplicationName.mockReturnValue(application); + mockRouteDefinitionParams.authz.applicationName = application; mockRouteDefinitionParams.authz.privileges.get.mockReturnValue(privilegeMap); const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); diff --git a/x-pack/plugins/security/server/routes/authorization/roles/put.ts b/x-pack/plugins/security/server/routes/authorization/roles/put.ts index 92c940132e660..e0245e7260446 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/put.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/put.ts @@ -42,7 +42,7 @@ export function definePutRolesRoutes({ router, authz, clusterClient }: RouteDefi const body = transformPutPayloadToElasticsearchRole( request.body, - authz.getApplicationName(), + authz.applicationName, rawRoles[name] ? rawRoles[name].applications : [] ); From aa31b535d1ff2f732deb9a3fd7d564a8e5f26431 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Wed, 11 Dec 2019 09:54:42 +0100 Subject: [PATCH 18/40] [Watcher] New Platform (NP) Migration (#50908) * First iteration of watch public -> new platform Still need to switch to np ready version of use_request * - Switched to using np ready request - Some updates after API changes * First attempt at server shim * Rename file and re-enable react hooks linting * Fix some public types and react hooks lint rules * Fix types * More ES lint react hooks fixes * Migrated server lib -> ts. Part way done with migrating routes to NP router and TS * Big subset of routes to TS and NP router - almost there * Delete legacy error wrappers and moved last set of routes to TS and NP router * Remove @ts-ignore's and update route registration to use shim with http router * Added routes validations, fixes for hooks and fixes for types * Fix more types and finish testing API routes * Fix usage of feature catalogue and fix time buckets types * Fix error message shape [skip ci] * Split legacy from new platform dependencies server-side * Refactor: Seperate client legacy and NP dependencies * Add file: added types file * Fix UISettings client type import * Update license pre-routing factory spec * Update variable names, use of I18nContext (use NP) and docs * Use NP elasticsearchclient * Simplify is_es_error_factory * Fix types * Improve code legibility and remove second use of `useAppContext` * Use @kbn/config-schema (not validate: false) on routes! * Fix watch create JSON spec * Create threshold test working * Unskip watch_edit.test.ts * Unskip watch_list.test.ts * Done re-enabling component integration tests * TimeBuckets typo + remove unnecessary // @ts-ignore --- .eslintrc.js | 7 - src/legacy/ui/public/time_buckets/index.d.ts | 22 +++ .../public/request/np_ready_request.ts | 9 +- x-pack/dev-tools/jest/create_jest_config.js | 3 +- .../helpers/app_context.mock.tsx | 50 ++++++ .../helpers/body_response.ts} | 4 +- .../helpers/http_requests.ts | 2 +- .../client_integration/helpers/index.ts | 2 +- .../helpers/setup_environment.ts | 12 +- .../helpers/watch_create_json.helpers.ts | 7 +- .../helpers/watch_create_threshold.helpers.ts | 7 +- .../helpers/watch_edit.helpers.ts | 7 +- .../helpers/watch_list.helpers.ts | 5 +- .../helpers/watch_status.helpers.ts | 5 +- .../watch_create_json.test.ts | 19 +-- .../watch_create_threshold.test.tsx | 130 ++++++---------- .../client_integration/watch_edit.test.ts | 33 +--- .../client_integration/watch_list.test.ts | 9 +- .../client_integration/watch_status.test.ts | 9 +- x-pack/legacy/plugins/watcher/kibana.json | 9 ++ .../plugins/watcher/plugin_definition.js | 46 ------ .../plugins/watcher/plugin_definition.ts | 32 ++++ x-pack/legacy/plugins/watcher/public/app.html | 3 - .../legacy/plugins/watcher/public/legacy.ts | 146 ++++++++++++++++++ .../documentation_links.ts | 24 --- ...fecycle.js => manage_angular_lifecycle.ts} | 11 +- .../plugins/watcher/public/models/index.d.ts | 42 ----- .../{app.js => np_ready/application/app.tsx} | 79 ++++++---- .../np_ready/application/app_context.tsx | 65 ++++++++ .../public/np_ready/application/boot.tsx | 35 +++++ .../components/confirm_watches_modal.tsx | 0 .../components/delete_watches_modal.tsx | 7 +- .../application}/components/form_errors.tsx | 0 .../application}/components/index.ts | 0 .../components/page_error/index.ts | 0 .../components/page_error/page_error.tsx | 0 .../page_error/page_error_forbidden.tsx | 0 .../page_error/page_error_not_exist.tsx | 0 .../application}/components/section_error.tsx | 20 ++- .../components/section_loading.tsx | 0 .../application}/components/watch_status.tsx | 2 +- .../application}/constants/base_path.ts | 0 .../application}/constants/index.ts | 0 .../{ => np_ready/application}/index.scss | 0 .../{ => np_ready/application}/lib/api.ts | 96 +++++------- .../application}/lib/breadcrumbs.ts | 0 .../application}/lib/format_date.ts | 0 .../application}/lib/get_search_value.ts | 0 .../application}/lib/get_time_unit_label.ts | 2 +- .../application}/lib/navigation.ts | 0 .../application}/lib/use_request.ts | 1 + .../application}/models/action/action.js | 2 +- .../application}/models/action/base_action.js | 0 .../models/action/email_action.js | 0 .../application}/models/action/index.js | 0 .../models/action/index_action.js | 0 .../application}/models/action/jira_action.js | 0 .../models/action/logging_action.js | 0 .../models/action/pagerduty_action.js | 0 .../models/action/slack_action.js | 0 .../models/action/unknown_action.js | 0 .../models/action/webhook_action.js | 0 .../models/action_status/action_status.js | 2 +- .../models/action_status/index.js | 0 .../models/execute_details/execute_details.js | 0 .../models/execute_details/index.js | 0 .../np_ready/application/models/index.d.ts | 39 +++++ .../application}/models/settings/index.js | 0 .../application}/models/settings/settings.js | 0 .../models/visualize_options/index.js | 0 .../visualize_options/visualize_options.js | 0 .../application}/models/watch/agg_types.ts | 2 +- .../application}/models/watch/base_watch.js | 0 .../application}/models/watch/comparators.ts | 2 +- .../models/watch/default_watch.json | 0 .../models/watch/group_by_types.ts | 0 .../application}/models/watch/index.js | 0 .../application}/models/watch/json_watch.js | 2 +- .../check_action_id_collision.js | 0 .../lib/check_action_id_collision/index.js | 0 .../lib/create_action_id/create_action_id.js | 0 .../watch/lib/create_action_id/index.js | 0 .../models/watch/monitoring_watch.js | 2 +- .../models/watch/threshold_watch.js | 2 +- .../application}/models/watch/watch.js | 2 +- .../application}/models/watch_errors/index.js | 0 .../models/watch_errors/watch_errors.js | 0 .../models/watch_history_item/index.js | 0 .../watch_history_item/watch_history_item.js | 2 +- .../application}/models/watch_status/index.js | 0 .../models/watch_status/watch_status.js | 2 +- .../components/json_watch_edit/index.ts | 0 .../json_watch_edit/json_watch_edit.tsx | 8 +- .../json_watch_edit/json_watch_edit_form.tsx | 17 +- .../json_watch_edit_simulate.tsx | 13 +- .../json_watch_edit_simulate_results.tsx | 2 +- .../components/monitoring_watch_edit/index.ts | 0 .../monitoring_watch_edit.tsx | 0 .../watch_edit/components/request_flyout.tsx | 0 .../action_fields/email_action_fields.tsx | 2 +- .../action_fields/index.ts | 0 .../action_fields/index_action_fields.tsx | 2 +- .../action_fields/jira_action_fields.tsx | 2 +- .../action_fields/logging_action_fields.tsx | 2 +- .../action_fields/pagerduty_action_fields.tsx | 2 +- .../action_fields/slack_action_fields.tsx | 2 +- .../action_fields/webhook_action_fields.tsx | 4 +- .../components/threshold_watch_edit/index.ts | 0 .../threshold_watch_action_accordion.tsx | 23 +-- .../threshold_watch_action_dropdown.tsx | 4 +- .../threshold_watch_action_panel.tsx | 4 +- .../threshold_watch_edit.tsx | 66 ++++---- .../watch_visualization.tsx | 83 +++++----- .../watch_edit/components/watch_edit.tsx | 49 +++--- .../sections/watch_edit/watch_context.ts | 0 .../sections/watch_edit/watch_edit_actions.ts | 18 +-- .../watch_list/components/watch_list.tsx | 19 ++- .../watch_status/components/watch_detail.tsx | 9 +- .../watch_status/components/watch_history.tsx | 8 +- .../watch_status/components/watch_status.tsx | 19 ++- .../watch_status/watch_details_context.ts | 0 .../application}/shared_imports.ts | 2 +- .../public/{index.js => np_ready/index.ts} | 4 +- .../plugins/watcher/public/np_ready/plugin.ts | 62 ++++++++ .../np_ready/types.ts} | 8 +- .../watcher/public/register_feature.js | 24 --- .../watcher/public/register_feature.ts | 21 +++ .../public/register_management_sections.js | 60 ------- .../plugins/watcher/public/register_route.js | 68 -------- .../call_with_internal_user_factory.js | 18 --- .../call_with_request_factory.js | 21 --- .../lib/call_with_request_factory/index.js | 7 - .../lib/elasticsearch_js_plugin/index.js | 7 - .../__tests__/wrap_custom_error.js | 21 --- .../error_wrappers/__tests__/wrap_es_error.js | 39 ----- .../__tests__/wrap_unknown_error.js | 19 --- .../server/lib/error_wrappers/index.js | 9 -- .../lib/error_wrappers/wrap_custom_error.js | 18 --- .../lib/error_wrappers/wrap_es_error.js | 30 ---- .../lib/error_wrappers/wrap_unknown_error.js | 17 -- .../__tests__/is_es_error_factory.js | 48 ------ .../server/lib/is_es_error_factory/index.js | 7 - .../is_es_error_factory.js | 18 --- .../license_pre_routing_factory.js | 31 ---- .../index.js => np_ready/index.ts} | 4 +- .../np_ready/lib/call_with_request_factory.ts | 28 ++++ .../lib/elasticsearch_js_plugin.ts} | 98 ++++++------ .../__tests__/fetch_all_from_scroll.js | 0 .../fetch_all_from_scroll.ts} | 15 +- .../lib/fetch_all_from_scroll/index.ts} | 0 .../np_ready/lib/is_es_error}/index.ts | 2 +- .../lib/is_es_error/is_es_error.ts} | 8 +- .../__tests__/license_pre_routing_factory.js | 23 +-- .../lib/license_pre_routing_factory/index.ts} | 0 .../license_pre_routing_factory.ts | 43 ++++++ .../lib/normalized_field_types/index.ts} | 0 .../normalized_field_types.ts} | 16 +- .../action_status/__tests__/action_status.js | 2 +- .../models/action_status/action_status.js | 4 +- .../models/action_status/index.js | 0 .../__tests__/execute_details.js | 0 .../models/execute_details/execute_details.js | 0 .../models/execute_details/index.js | 0 .../models/fields/__tests__/fields.js | 0 .../{ => np_ready}/models/fields/fields.js | 0 .../{ => np_ready}/models/fields/index.js | 0 .../models/settings/__tests__/settings.js | 0 .../{ => np_ready}/models/settings/index.js | 0 .../models/settings/settings.js | 2 +- .../models/visualize_options/index.js | 0 .../visualize_options/visualize_options.js | 0 .../{ => np_ready}/models/watch/base_watch.js | 2 +- .../models/watch/base_watch.test.js | 0 .../{ => np_ready}/models/watch/index.js | 0 .../{ => np_ready}/models/watch/json_watch.js | 4 +- .../models/watch/json_watch.test.js | 0 .../lib/get_watch_type/get_watch_type.js | 2 +- .../models/watch/lib/get_watch_type/index.js | 0 .../models/watch/monitoring_watch.js | 2 +- .../models/watch/monitoring_watch.test.js | 0 .../__tests__/format_visualize_data.js | 2 +- .../threshold_watch/build_visualize_query.js | 4 +- .../threshold_watch/data_samples/count.json | 0 .../data_samples/count.query.date.json | 0 .../data_samples/count.query.json | 0 .../data_samples/count_terms.json | 0 .../data_samples/count_terms.query.date.json | 0 .../data_samples/count_terms.query.json | 0 .../data_samples/non_count.json | 0 .../data_samples/non_count.query.date.json | 0 .../data_samples/non_count.query.json | 0 .../data_samples/non_count_terms.json | 0 .../non_count_terms.query.date.json | 0 .../data_samples/non_count_terms.query.json | 0 .../threshold_watch/format_visualize_data.js | 2 +- .../models/watch/threshold_watch/index.js | 0 .../watch/threshold_watch/threshold_watch.js | 4 +- .../threshold_watch/threshold_watch.test.js | 2 +- .../{ => np_ready}/models/watch/watch.js | 2 +- .../{ => np_ready}/models/watch/watch.test.js | 2 +- .../models/watch_errors/index.js | 0 .../models/watch_errors/watch_errors.js | 0 .../models/watch_errors/watch_errors.test.js | 0 .../__tests__/watch_history_item.js | 0 .../models/watch_history_item/index.js | 0 .../watch_history_item/watch_history_item.js | 2 +- .../watch_status/__tests__/watch_status.js | 2 +- .../models/watch_status/index.js | 0 .../models/watch_status/watch_status.js | 4 +- .../plugins/watcher/server/np_ready/plugin.ts | 51 ++++++ .../routes/api/indices/index.ts} | 0 .../routes/api/indices/register_get_route.ts | 92 +++++++++++ .../api/indices/register_indices_routes.ts} | 5 +- .../routes/api/license/index.ts} | 0 .../api/license/register_license_routes.ts} | 5 +- .../api/license/register_refresh_route.ts} | 25 +-- .../routes/api/register_list_fields_route.ts | 65 ++++++++ .../routes/api/register_load_history_route.ts | 77 +++++++++ .../routes/api/settings/index.ts} | 0 .../api/settings/register_load_route.ts | 43 ++++++ .../api/settings/register_settings_routes.ts} | 5 +- .../routes/api/watch/action/index.ts} | 0 .../action/register_acknowledge_route.ts | 65 ++++++++ .../watch/action/register_action_routes.ts} | 5 +- .../routes/api/watch/index.ts} | 0 .../api/watch/register_activate_route.ts | 66 ++++++++ .../api/watch/register_deactivate_route.ts | 65 ++++++++ .../routes/api/watch/register_delete_route.ts | 52 +++++++ .../api/watch/register_execute_route.ts | 78 ++++++++++ .../api/watch/register_history_route.ts | 97 ++++++++++++ .../routes/api/watch/register_load_route.ts | 69 +++++++++ .../routes/api/watch/register_save_route.ts | 104 +++++++++++++ .../api/watch/register_visualize_route.ts | 70 +++++++++ .../api/watch/register_watch_routes.ts} | 21 +-- .../routes/api/watches/index.ts} | 0 .../api/watches/register_delete_route.ts | 63 ++++++++ .../routes/api/watches/register_list_route.ts | 86 +++++++++++ .../api/watches/register_watches_routes.ts} | 7 +- .../plugins/watcher/server/np_ready/types.ts | 22 +++ .../routes/api/fields/register_list_route.js | 60 ------- .../server/routes/api/history/index.js | 7 - .../routes/api/history/register_load_route.js | 78 ---------- .../routes/api/indices/register_get_route.js | 89 ----------- .../api/settings/register_load_route.js | 47 ------ .../action/register_acknowledge_route.js | 63 -------- .../api/watch/register_activate_route.js | 63 -------- .../api/watch/register_deactivate_route.js | 63 -------- .../routes/api/watch/register_delete_route.js | 50 ------ .../api/watch/register_execute_route.js | 69 --------- .../api/watch/register_history_route.js | 89 ----------- .../routes/api/watch/register_load_route.js | 68 -------- .../routes/api/watch/register_save_route.js | 94 ----------- .../api/watch/register_visualize_route.js | 62 -------- .../api/watches/register_delete_route.js | 58 ------- .../routes/api/watches/register_list_route.js | 79 ---------- 255 files changed, 2290 insertions(+), 2206 deletions(-) create mode 100644 src/legacy/ui/public/time_buckets/index.d.ts create mode 100644 x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/app_context.mock.tsx rename x-pack/legacy/plugins/watcher/{server/routes/api/fields/index.js => __jest__/client_integration/helpers/body_response.ts} (56%) create mode 100644 x-pack/legacy/plugins/watcher/kibana.json delete mode 100644 x-pack/legacy/plugins/watcher/plugin_definition.js create mode 100644 x-pack/legacy/plugins/watcher/plugin_definition.ts delete mode 100644 x-pack/legacy/plugins/watcher/public/app.html create mode 100644 x-pack/legacy/plugins/watcher/public/legacy.ts delete mode 100644 x-pack/legacy/plugins/watcher/public/lib/documentation_links/documentation_links.ts rename x-pack/legacy/plugins/watcher/public/{lib/manage_angular_lifecycle.js => manage_angular_lifecycle.ts} (75%) delete mode 100644 x-pack/legacy/plugins/watcher/public/models/index.d.ts rename x-pack/legacy/plugins/watcher/public/{app.js => np_ready/application/app.tsx} (60%) create mode 100644 x-pack/legacy/plugins/watcher/public/np_ready/application/app_context.tsx create mode 100644 x-pack/legacy/plugins/watcher/public/np_ready/application/boot.tsx rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/components/confirm_watches_modal.tsx (100%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/components/delete_watches_modal.tsx (95%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/components/form_errors.tsx (100%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/components/index.ts (100%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/components/page_error/index.ts (100%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/components/page_error/page_error.tsx (100%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/components/page_error/page_error_forbidden.tsx (100%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/components/page_error/page_error_not_exist.tsx (100%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/components/section_error.tsx (80%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/components/section_loading.tsx (100%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/components/watch_status.tsx (95%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/constants/base_path.ts (100%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/constants/index.ts (100%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/index.scss (100%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/lib/api.ts (61%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/lib/breadcrumbs.ts (100%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/lib/format_date.ts (100%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/lib/get_search_value.ts (100%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/lib/get_time_unit_label.ts (95%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/lib/navigation.ts (100%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/lib/use_request.ts (99%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/models/action/action.js (95%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/models/action/base_action.js (100%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/models/action/email_action.js (100%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/models/action/index.js (100%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/models/action/index_action.js (100%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/models/action/jira_action.js (100%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/models/action/logging_action.js (100%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/models/action/pagerduty_action.js (100%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/models/action/slack_action.js (100%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/models/action/unknown_action.js (100%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/models/action/webhook_action.js (100%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/models/action_status/action_status.js (95%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/models/action_status/index.js (100%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/models/execute_details/execute_details.js (100%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/models/execute_details/index.js (100%) create mode 100644 x-pack/legacy/plugins/watcher/public/np_ready/application/models/index.d.ts rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/models/settings/index.js (100%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/models/settings/settings.js (100%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/models/visualize_options/index.js (100%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/models/visualize_options/visualize_options.js (100%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/models/watch/agg_types.ts (94%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/models/watch/base_watch.js (100%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/models/watch/comparators.ts (96%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/models/watch/default_watch.json (100%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/models/watch/group_by_types.ts (100%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/models/watch/index.js (100%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/models/watch/json_watch.js (98%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/models/watch/lib/check_action_id_collision/check_action_id_collision.js (100%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/models/watch/lib/check_action_id_collision/index.js (100%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/models/watch/lib/create_action_id/create_action_id.js (100%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/models/watch/lib/create_action_id/index.js (100%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/models/watch/monitoring_watch.js (92%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/models/watch/threshold_watch.js (99%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/models/watch/watch.js (93%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/models/watch_errors/index.js (100%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/models/watch_errors/watch_errors.js (100%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/models/watch_history_item/index.js (100%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/models/watch_history_item/watch_history_item.js (91%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/models/watch_status/index.js (100%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/models/watch_status/watch_status.js (94%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/sections/watch_edit/components/json_watch_edit/index.ts (100%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/sections/watch_edit/components/json_watch_edit/json_watch_edit.tsx (92%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/sections/watch_edit/components/json_watch_edit/json_watch_edit_form.tsx (94%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/sections/watch_edit/components/json_watch_edit/json_watch_edit_simulate.tsx (96%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/sections/watch_edit/components/json_watch_edit/json_watch_edit_simulate_results.tsx (99%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/sections/watch_edit/components/monitoring_watch_edit/index.ts (100%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/sections/watch_edit/components/monitoring_watch_edit/monitoring_watch_edit.tsx (100%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/sections/watch_edit/components/request_flyout.tsx (100%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/sections/watch_edit/components/threshold_watch_edit/action_fields/email_action_fields.tsx (97%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/sections/watch_edit/components/threshold_watch_edit/action_fields/index.ts (100%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/sections/watch_edit/components/threshold_watch_edit/action_fields/index_action_fields.tsx (94%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/sections/watch_edit/components/threshold_watch_edit/action_fields/jira_action_fields.tsx (97%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/sections/watch_edit/components/threshold_watch_edit/action_fields/logging_action_fields.tsx (94%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/sections/watch_edit/components/threshold_watch_edit/action_fields/pagerduty_action_fields.tsx (95%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/sections/watch_edit/components/threshold_watch_edit/action_fields/slack_action_fields.tsx (96%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/sections/watch_edit/components/threshold_watch_edit/action_fields/webhook_action_fields.tsx (98%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/sections/watch_edit/components/threshold_watch_edit/index.ts (100%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/sections/watch_edit/components/threshold_watch_edit/threshold_watch_action_accordion.tsx (91%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/sections/watch_edit/components/threshold_watch_edit/threshold_watch_action_dropdown.tsx (96%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/sections/watch_edit/components/threshold_watch_edit/threshold_watch_action_panel.tsx (93%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/sections/watch_edit/components/threshold_watch_edit/threshold_watch_edit.tsx (95%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/sections/watch_edit/components/threshold_watch_edit/watch_visualization.tsx (83%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/sections/watch_edit/components/watch_edit.tsx (82%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/sections/watch_edit/watch_context.ts (100%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/sections/watch_edit/watch_edit_actions.ts (86%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/sections/watch_list/components/watch_list.tsx (97%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/sections/watch_status/components/watch_detail.tsx (96%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/sections/watch_status/components/watch_history.tsx (97%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/sections/watch_status/components/watch_status.tsx (94%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/sections/watch_status/watch_details_context.ts (100%) rename x-pack/legacy/plugins/watcher/public/{ => np_ready/application}/shared_imports.ts (79%) rename x-pack/legacy/plugins/watcher/public/{index.js => np_ready/index.ts} (71%) create mode 100644 x-pack/legacy/plugins/watcher/public/np_ready/plugin.ts rename x-pack/legacy/plugins/watcher/{server/routes/api/fields/register_fields_routes.js => public/np_ready/types.ts} (63%) delete mode 100644 x-pack/legacy/plugins/watcher/public/register_feature.js create mode 100644 x-pack/legacy/plugins/watcher/public/register_feature.ts delete mode 100644 x-pack/legacy/plugins/watcher/public/register_management_sections.js delete mode 100644 x-pack/legacy/plugins/watcher/public/register_route.js delete mode 100644 x-pack/legacy/plugins/watcher/server/lib/call_with_internal_user_factory/call_with_internal_user_factory.js delete mode 100644 x-pack/legacy/plugins/watcher/server/lib/call_with_request_factory/call_with_request_factory.js delete mode 100644 x-pack/legacy/plugins/watcher/server/lib/call_with_request_factory/index.js delete mode 100644 x-pack/legacy/plugins/watcher/server/lib/elasticsearch_js_plugin/index.js delete mode 100644 x-pack/legacy/plugins/watcher/server/lib/error_wrappers/__tests__/wrap_custom_error.js delete mode 100644 x-pack/legacy/plugins/watcher/server/lib/error_wrappers/__tests__/wrap_es_error.js delete mode 100644 x-pack/legacy/plugins/watcher/server/lib/error_wrappers/__tests__/wrap_unknown_error.js delete mode 100644 x-pack/legacy/plugins/watcher/server/lib/error_wrappers/index.js delete mode 100644 x-pack/legacy/plugins/watcher/server/lib/error_wrappers/wrap_custom_error.js delete mode 100644 x-pack/legacy/plugins/watcher/server/lib/error_wrappers/wrap_es_error.js delete mode 100644 x-pack/legacy/plugins/watcher/server/lib/error_wrappers/wrap_unknown_error.js delete mode 100644 x-pack/legacy/plugins/watcher/server/lib/is_es_error_factory/__tests__/is_es_error_factory.js delete mode 100644 x-pack/legacy/plugins/watcher/server/lib/is_es_error_factory/index.js delete mode 100644 x-pack/legacy/plugins/watcher/server/lib/is_es_error_factory/is_es_error_factory.js delete mode 100644 x-pack/legacy/plugins/watcher/server/lib/license_pre_routing_factory/license_pre_routing_factory.js rename x-pack/legacy/plugins/watcher/server/{lib/call_with_internal_user_factory/index.js => np_ready/index.ts} (55%) create mode 100644 x-pack/legacy/plugins/watcher/server/np_ready/lib/call_with_request_factory.ts rename x-pack/legacy/plugins/watcher/server/{lib/elasticsearch_js_plugin/elasticsearch_js_plugin.js => np_ready/lib/elasticsearch_js_plugin.ts} (84%) rename x-pack/legacy/plugins/watcher/server/{ => np_ready}/lib/fetch_all_from_scroll/__tests__/fetch_all_from_scroll.js (100%) rename x-pack/legacy/plugins/watcher/server/{lib/fetch_all_from_scroll/fetch_all_from_scroll.js => np_ready/lib/fetch_all_from_scroll/fetch_all_from_scroll.ts} (64%) rename x-pack/legacy/plugins/watcher/server/{lib/fetch_all_from_scroll/index.js => np_ready/lib/fetch_all_from_scroll/index.ts} (100%) rename x-pack/legacy/plugins/watcher/{public/lib/documentation_links => server/np_ready/lib/is_es_error}/index.ts (84%) rename x-pack/legacy/plugins/watcher/server/{routes/api/history/register_history_routes.js => np_ready/lib/is_es_error/is_es_error.ts} (55%) rename x-pack/legacy/plugins/watcher/server/{ => np_ready}/lib/license_pre_routing_factory/__tests__/license_pre_routing_factory.js (71%) rename x-pack/legacy/plugins/watcher/server/{lib/license_pre_routing_factory/index.js => np_ready/lib/license_pre_routing_factory/index.ts} (100%) create mode 100644 x-pack/legacy/plugins/watcher/server/np_ready/lib/license_pre_routing_factory/license_pre_routing_factory.ts rename x-pack/legacy/plugins/watcher/server/{lib/normalized_field_types/index.js => np_ready/lib/normalized_field_types/index.ts} (100%) rename x-pack/legacy/plugins/watcher/server/{lib/normalized_field_types/normalized_field_types.js => np_ready/lib/normalized_field_types/normalized_field_types.ts} (61%) rename x-pack/legacy/plugins/watcher/server/{ => np_ready}/models/action_status/__tests__/action_status.js (99%) rename x-pack/legacy/plugins/watcher/server/{ => np_ready}/models/action_status/action_status.js (97%) rename x-pack/legacy/plugins/watcher/server/{ => np_ready}/models/action_status/index.js (100%) rename x-pack/legacy/plugins/watcher/server/{ => np_ready}/models/execute_details/__tests__/execute_details.js (100%) rename x-pack/legacy/plugins/watcher/server/{ => np_ready}/models/execute_details/execute_details.js (100%) rename x-pack/legacy/plugins/watcher/server/{ => np_ready}/models/execute_details/index.js (100%) rename x-pack/legacy/plugins/watcher/server/{ => np_ready}/models/fields/__tests__/fields.js (100%) rename x-pack/legacy/plugins/watcher/server/{ => np_ready}/models/fields/fields.js (100%) rename x-pack/legacy/plugins/watcher/server/{ => np_ready}/models/fields/index.js (100%) rename x-pack/legacy/plugins/watcher/server/{ => np_ready}/models/settings/__tests__/settings.js (100%) rename x-pack/legacy/plugins/watcher/server/{ => np_ready}/models/settings/index.js (100%) rename x-pack/legacy/plugins/watcher/server/{ => np_ready}/models/settings/settings.js (97%) rename x-pack/legacy/plugins/watcher/server/{ => np_ready}/models/visualize_options/index.js (100%) rename x-pack/legacy/plugins/watcher/server/{ => np_ready}/models/visualize_options/visualize_options.js (100%) rename x-pack/legacy/plugins/watcher/server/{ => np_ready}/models/watch/base_watch.js (98%) rename x-pack/legacy/plugins/watcher/server/{ => np_ready}/models/watch/base_watch.test.js (100%) rename x-pack/legacy/plugins/watcher/server/{ => np_ready}/models/watch/index.js (100%) rename x-pack/legacy/plugins/watcher/server/{ => np_ready}/models/watch/json_watch.js (93%) rename x-pack/legacy/plugins/watcher/server/{ => np_ready}/models/watch/json_watch.test.js (100%) rename x-pack/legacy/plugins/watcher/server/{ => np_ready}/models/watch/lib/get_watch_type/get_watch_type.js (88%) rename x-pack/legacy/plugins/watcher/server/{ => np_ready}/models/watch/lib/get_watch_type/index.js (100%) rename x-pack/legacy/plugins/watcher/server/{ => np_ready}/models/watch/monitoring_watch.js (97%) rename x-pack/legacy/plugins/watcher/server/{ => np_ready}/models/watch/monitoring_watch.test.js (100%) rename x-pack/legacy/plugins/watcher/server/{ => np_ready}/models/watch/threshold_watch/__tests__/format_visualize_data.js (99%) rename x-pack/legacy/plugins/watcher/server/{ => np_ready}/models/watch/threshold_watch/build_visualize_query.js (95%) rename x-pack/legacy/plugins/watcher/server/{ => np_ready}/models/watch/threshold_watch/data_samples/count.json (100%) rename x-pack/legacy/plugins/watcher/server/{ => np_ready}/models/watch/threshold_watch/data_samples/count.query.date.json (100%) rename x-pack/legacy/plugins/watcher/server/{ => np_ready}/models/watch/threshold_watch/data_samples/count.query.json (100%) rename x-pack/legacy/plugins/watcher/server/{ => np_ready}/models/watch/threshold_watch/data_samples/count_terms.json (100%) rename x-pack/legacy/plugins/watcher/server/{ => np_ready}/models/watch/threshold_watch/data_samples/count_terms.query.date.json (100%) rename x-pack/legacy/plugins/watcher/server/{ => np_ready}/models/watch/threshold_watch/data_samples/count_terms.query.json (100%) rename x-pack/legacy/plugins/watcher/server/{ => np_ready}/models/watch/threshold_watch/data_samples/non_count.json (100%) rename x-pack/legacy/plugins/watcher/server/{ => np_ready}/models/watch/threshold_watch/data_samples/non_count.query.date.json (100%) rename x-pack/legacy/plugins/watcher/server/{ => np_ready}/models/watch/threshold_watch/data_samples/non_count.query.json (100%) rename x-pack/legacy/plugins/watcher/server/{ => np_ready}/models/watch/threshold_watch/data_samples/non_count_terms.json (100%) rename x-pack/legacy/plugins/watcher/server/{ => np_ready}/models/watch/threshold_watch/data_samples/non_count_terms.query.date.json (100%) rename x-pack/legacy/plugins/watcher/server/{ => np_ready}/models/watch/threshold_watch/data_samples/non_count_terms.query.json (100%) rename x-pack/legacy/plugins/watcher/server/{ => np_ready}/models/watch/threshold_watch/format_visualize_data.js (97%) rename x-pack/legacy/plugins/watcher/server/{ => np_ready}/models/watch/threshold_watch/index.js (100%) rename x-pack/legacy/plugins/watcher/server/{ => np_ready}/models/watch/threshold_watch/threshold_watch.js (97%) rename x-pack/legacy/plugins/watcher/server/{ => np_ready}/models/watch/threshold_watch/threshold_watch.test.js (99%) rename x-pack/legacy/plugins/watcher/server/{ => np_ready}/models/watch/watch.js (97%) rename x-pack/legacy/plugins/watcher/server/{ => np_ready}/models/watch/watch.test.js (98%) rename x-pack/legacy/plugins/watcher/server/{ => np_ready}/models/watch_errors/index.js (100%) rename x-pack/legacy/plugins/watcher/server/{ => np_ready}/models/watch_errors/watch_errors.js (100%) rename x-pack/legacy/plugins/watcher/server/{ => np_ready}/models/watch_errors/watch_errors.test.js (100%) rename x-pack/legacy/plugins/watcher/server/{ => np_ready}/models/watch_history_item/__tests__/watch_history_item.js (100%) rename x-pack/legacy/plugins/watcher/server/{ => np_ready}/models/watch_history_item/index.js (100%) rename x-pack/legacy/plugins/watcher/server/{ => np_ready}/models/watch_history_item/watch_history_item.js (97%) rename x-pack/legacy/plugins/watcher/server/{ => np_ready}/models/watch_status/__tests__/watch_status.js (99%) rename x-pack/legacy/plugins/watcher/server/{ => np_ready}/models/watch_status/index.js (100%) rename x-pack/legacy/plugins/watcher/server/{ => np_ready}/models/watch_status/watch_status.js (98%) create mode 100644 x-pack/legacy/plugins/watcher/server/np_ready/plugin.ts rename x-pack/legacy/plugins/watcher/server/{routes/api/indices/index.js => np_ready/routes/api/indices/index.ts} (100%) create mode 100644 x-pack/legacy/plugins/watcher/server/np_ready/routes/api/indices/register_get_route.ts rename x-pack/legacy/plugins/watcher/server/{routes/api/indices/register_indices_routes.js => np_ready/routes/api/indices/register_indices_routes.ts} (62%) rename x-pack/legacy/plugins/watcher/server/{routes/api/license/index.js => np_ready/routes/api/license/index.ts} (100%) rename x-pack/legacy/plugins/watcher/server/{routes/api/license/register_license_routes.js => np_ready/routes/api/license/register_license_routes.ts} (62%) rename x-pack/legacy/plugins/watcher/server/{routes/api/license/register_refresh_route.js => np_ready/routes/api/license/register_refresh_route.ts} (50%) create mode 100644 x-pack/legacy/plugins/watcher/server/np_ready/routes/api/register_list_fields_route.ts create mode 100644 x-pack/legacy/plugins/watcher/server/np_ready/routes/api/register_load_history_route.ts rename x-pack/legacy/plugins/watcher/server/{routes/api/settings/index.js => np_ready/routes/api/settings/index.ts} (100%) create mode 100644 x-pack/legacy/plugins/watcher/server/np_ready/routes/api/settings/register_load_route.ts rename x-pack/legacy/plugins/watcher/server/{routes/api/settings/register_settings_routes.js => np_ready/routes/api/settings/register_settings_routes.ts} (62%) rename x-pack/legacy/plugins/watcher/server/{routes/api/watch/action/index.js => np_ready/routes/api/watch/action/index.ts} (100%) create mode 100644 x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/action/register_acknowledge_route.ts rename x-pack/legacy/plugins/watcher/server/{routes/api/watch/action/register_action_routes.js => np_ready/routes/api/watch/action/register_action_routes.ts} (61%) rename x-pack/legacy/plugins/watcher/server/{routes/api/watch/index.js => np_ready/routes/api/watch/index.ts} (100%) create mode 100644 x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/register_activate_route.ts create mode 100644 x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/register_deactivate_route.ts create mode 100644 x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/register_delete_route.ts create mode 100644 x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/register_execute_route.ts create mode 100644 x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/register_history_route.ts create mode 100644 x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/register_load_route.ts create mode 100644 x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/register_save_route.ts create mode 100644 x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/register_visualize_route.ts rename x-pack/legacy/plugins/watcher/server/{routes/api/watch/register_watch_routes.js => np_ready/routes/api/watch/register_watch_routes.ts} (62%) rename x-pack/legacy/plugins/watcher/server/{routes/api/watches/index.js => np_ready/routes/api/watches/index.ts} (100%) create mode 100644 x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watches/register_delete_route.ts create mode 100644 x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watches/register_list_route.ts rename x-pack/legacy/plugins/watcher/server/{routes/api/watches/register_watches_routes.js => np_ready/routes/api/watches/register_watches_routes.ts} (62%) create mode 100644 x-pack/legacy/plugins/watcher/server/np_ready/types.ts delete mode 100644 x-pack/legacy/plugins/watcher/server/routes/api/fields/register_list_route.js delete mode 100644 x-pack/legacy/plugins/watcher/server/routes/api/history/index.js delete mode 100644 x-pack/legacy/plugins/watcher/server/routes/api/history/register_load_route.js delete mode 100644 x-pack/legacy/plugins/watcher/server/routes/api/indices/register_get_route.js delete mode 100644 x-pack/legacy/plugins/watcher/server/routes/api/settings/register_load_route.js delete mode 100644 x-pack/legacy/plugins/watcher/server/routes/api/watch/action/register_acknowledge_route.js delete mode 100644 x-pack/legacy/plugins/watcher/server/routes/api/watch/register_activate_route.js delete mode 100644 x-pack/legacy/plugins/watcher/server/routes/api/watch/register_deactivate_route.js delete mode 100644 x-pack/legacy/plugins/watcher/server/routes/api/watch/register_delete_route.js delete mode 100644 x-pack/legacy/plugins/watcher/server/routes/api/watch/register_execute_route.js delete mode 100644 x-pack/legacy/plugins/watcher/server/routes/api/watch/register_history_route.js delete mode 100644 x-pack/legacy/plugins/watcher/server/routes/api/watch/register_load_route.js delete mode 100644 x-pack/legacy/plugins/watcher/server/routes/api/watch/register_save_route.js delete mode 100644 x-pack/legacy/plugins/watcher/server/routes/api/watch/register_visualize_route.js delete mode 100644 x-pack/legacy/plugins/watcher/server/routes/api/watches/register_delete_route.js delete mode 100644 x-pack/legacy/plugins/watcher/server/routes/api/watches/register_list_route.js diff --git a/.eslintrc.js b/.eslintrc.js index 106724c323d30..e01632815bc68 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -209,13 +209,6 @@ module.exports = { 'react-hooks/rules-of-hooks': 'off', }, }, - { - files: ['x-pack/legacy/plugins/watcher/**/*.{js,ts,tsx}'], - rules: { - 'react-hooks/rules-of-hooks': 'off', - 'react-hooks/exhaustive-deps': 'off', - }, - }, /** * Prettier diff --git a/src/legacy/ui/public/time_buckets/index.d.ts b/src/legacy/ui/public/time_buckets/index.d.ts new file mode 100644 index 0000000000000..70b9495b81f0e --- /dev/null +++ b/src/legacy/ui/public/time_buckets/index.d.ts @@ -0,0 +1,22 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +declare module 'ui/time_buckets' { + export const TimeBuckets: any; +} diff --git a/src/plugins/es_ui_shared/public/request/np_ready_request.ts b/src/plugins/es_ui_shared/public/request/np_ready_request.ts index 48c7904661e51..5a3f28ed76486 100644 --- a/src/plugins/es_ui_shared/public/request/np_ready_request.ts +++ b/src/plugins/es_ui_shared/public/request/np_ready_request.ts @@ -19,11 +19,12 @@ import { useEffect, useState, useRef } from 'react'; -import { HttpServiceBase } from '../../../../../src/core/public'; +import { HttpServiceBase, HttpFetchQuery } from '../../../../../src/core/public'; export interface SendRequestConfig { path: string; method: 'get' | 'post' | 'put' | 'delete' | 'patch' | 'head'; + query?: HttpFetchQuery; body?: any; } @@ -48,10 +49,10 @@ export interface UseRequestResponse { export const sendRequest = async ( httpClient: HttpServiceBase, - { path, method, body }: SendRequestConfig + { path, method, body, query }: SendRequestConfig ): Promise => { try { - const response = await httpClient[method](path, { body }); + const response = await httpClient[method](path, { body, query }); return { data: response.data ? response.data : response, @@ -70,6 +71,7 @@ export const useRequest = ( { path, method, + query, body, pollIntervalMs, initialData, @@ -112,6 +114,7 @@ export const useRequest = ( const requestBody = { path, method, + query, body, }; diff --git a/x-pack/dev-tools/jest/create_jest_config.js b/x-pack/dev-tools/jest/create_jest_config.js index 199232262773d..f8d07668d0aae 100644 --- a/x-pack/dev-tools/jest/create_jest_config.js +++ b/x-pack/dev-tools/jest/create_jest_config.js @@ -20,7 +20,8 @@ export function createJestConfig({ kibanaDirectory, xPackKibanaDirectory }) { 'uiExports/(.*)': fileMockPath, '^src/core/(.*)': `${kibanaDirectory}/src/core/$1`, '^src/legacy/(.*)': `${kibanaDirectory}/src/legacy/$1`, - '^plugins/watcher/models/(.*)': `${xPackKibanaDirectory}/legacy/plugins/watcher/public/models/$1`, + '^plugins/watcher/np_ready/application/models/(.*)': + `${xPackKibanaDirectory}/legacy/plugins/watcher/public/np_ready/application/models/$1`, '^plugins/([^/.]*)(.*)': `${kibanaDirectory}/src/legacy/core_plugins/$1/public$2`, '^legacy/plugins/xpack_main/(.*);': `${xPackKibanaDirectory}/legacy/plugins/xpack_main/public/$1`, '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': fileMockPath, diff --git a/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/app_context.mock.tsx b/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/app_context.mock.tsx new file mode 100644 index 0000000000000..de285ee15b59d --- /dev/null +++ b/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/app_context.mock.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { ComponentType } from 'enzyme'; +import { + chromeServiceMock, + docLinksServiceMock, + uiSettingsServiceMock, + notificationServiceMock, + httpServiceMock, +} from '../../../../../../../src/core/public/mocks'; +import { AppContextProvider } from '../../../public/np_ready/application/app_context'; + +export const mockContextValue = { + docLinks: docLinksServiceMock.createStartContract(), + chrome: chromeServiceMock.createStartContract(), + legacy: { + TimeBuckets: class MockTimeBuckets { + setBounds(_domain: any) { + return {}; + } + getInterval() { + return { + expression: {}, + }; + } + }, + MANAGEMENT_BREADCRUMB: { text: 'test' }, + licenseStatus: {}, + }, + uiSettings: uiSettingsServiceMock.createSetupContract(), + toasts: notificationServiceMock.createSetupContract().toasts, + euiUtils: { + useChartsTheme: jest.fn(), + }, + // For our test harness, we don't use this mocked out http service + http: httpServiceMock.createSetupContract(), +}; + +export const withAppContext = (Component: ComponentType) => (props: any) => { + return ( + + + + ); +}; diff --git a/x-pack/legacy/plugins/watcher/server/routes/api/fields/index.js b/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/body_response.ts similarity index 56% rename from x-pack/legacy/plugins/watcher/server/routes/api/fields/index.js rename to x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/body_response.ts index 8474f8a614bfb..3b3df5fd6f879 100644 --- a/x-pack/legacy/plugins/watcher/server/routes/api/fields/index.js +++ b/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/body_response.ts @@ -4,4 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -export { registerFieldsRoutes } from './register_fields_routes'; +export const wrapBodyResponse = (obj: object) => JSON.stringify({ body: JSON.stringify(obj) }); + +export const unwrapBodyResponse = (string: string) => JSON.parse(JSON.parse(string).body); diff --git a/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/http_requests.ts b/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/http_requests.ts index 2170559dace5a..7d9c1e4163d7b 100644 --- a/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/http_requests.ts +++ b/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/http_requests.ts @@ -34,7 +34,7 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { const defaultResponse = { watchHistoryItems: [] }; server.respondWith( 'GET', - `${API_ROOT}/watch/:id/history?startTime=*`, + `${API_ROOT}/watch/:id/history`, mockResponse(defaultResponse, response) ); }; diff --git a/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/index.ts b/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/index.ts index ad005078db0a8..814028fe599ff 100644 --- a/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/index.ts +++ b/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/index.ts @@ -11,7 +11,7 @@ import { setup as watchCreateThresholdSetup } from './watch_create_threshold.hel import { setup as watchEditSetup } from './watch_edit.helpers'; export { nextTick, getRandomString, findTestSubject, TestBed } from '../../../../../../test_utils'; - +export { wrapBodyResponse, unwrapBodyResponse } from './body_response'; export { setupEnvironment } from './setup_environment'; export const pageHelpers = { diff --git a/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/setup_environment.ts b/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/setup_environment.ts index 806840a7821fd..7e748073c1c6b 100644 --- a/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/setup_environment.ts +++ b/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/setup_environment.ts @@ -7,9 +7,17 @@ import axios from 'axios'; import axiosXhrAdapter from 'axios/lib/adapters/xhr'; import { init as initHttpRequests } from './http_requests'; -import { setHttpClient, setSavedObjectsClient } from '../../../public/lib/api'; +import { setHttpClient, setSavedObjectsClient } from '../../../public/np_ready/application/lib/api'; const mockHttpClient = axios.create({ adapter: axiosXhrAdapter }); +mockHttpClient.interceptors.response.use( + res => { + return res.data; + }, + rej => { + return Promise.reject(rej); + } +); const mockSavedObjectsClient = () => { return { @@ -23,7 +31,7 @@ export const setupEnvironment = () => { // @ts-ignore setHttpClient(mockHttpClient); - setSavedObjectsClient(mockSavedObjectsClient()); + setSavedObjectsClient(mockSavedObjectsClient() as any); return { server, diff --git a/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/watch_create_json.helpers.ts b/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/watch_create_json.helpers.ts index bea215281a4bc..dafcf3a7070d2 100644 --- a/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/watch_create_json.helpers.ts +++ b/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/watch_create_json.helpers.ts @@ -3,10 +3,11 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { withAppContext } from './app_context.mock'; import { registerTestBed, TestBed, TestBedConfig } from '../../../../../../test_utils'; -import { WatchEdit } from '../../../public/sections/watch_edit/components/watch_edit'; +import { WatchEdit } from '../../../public/np_ready/application/sections/watch_edit/components/watch_edit'; import { ROUTES, WATCH_TYPES } from '../../../common/constants'; -import { registerRouter } from '../../../public/lib/navigation'; +import { registerRouter } from '../../../public/np_ready/application/lib/navigation'; const testBedConfig: TestBedConfig = { memoryRouter: { @@ -17,7 +18,7 @@ const testBedConfig: TestBedConfig = { doMountAsync: true, }; -const initTestBed = registerTestBed(WatchEdit, testBedConfig); +const initTestBed = registerTestBed(withAppContext(WatchEdit), testBedConfig); export interface WatchCreateJsonTestBed extends TestBed { actions: { diff --git a/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/watch_create_threshold.helpers.ts b/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/watch_create_threshold.helpers.ts index e33ae02036224..8cebe8ce26229 100644 --- a/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/watch_create_threshold.helpers.ts +++ b/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/watch_create_threshold.helpers.ts @@ -4,9 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ import { registerTestBed, TestBed, TestBedConfig } from '../../../../../../test_utils'; -import { WatchEdit } from '../../../public/sections/watch_edit/components/watch_edit'; +import { WatchEdit } from '../../../public/np_ready/application/sections/watch_edit/components/watch_edit'; import { ROUTES, WATCH_TYPES } from '../../../common/constants'; -import { registerRouter } from '../../../public/lib/navigation'; +import { registerRouter } from '../../../public/np_ready/application/lib/navigation'; +import { withAppContext } from './app_context.mock'; const testBedConfig: TestBedConfig = { memoryRouter: { @@ -17,7 +18,7 @@ const testBedConfig: TestBedConfig = { doMountAsync: true, }; -const initTestBed = registerTestBed(WatchEdit, testBedConfig); +const initTestBed = registerTestBed(withAppContext(WatchEdit), testBedConfig); export interface WatchCreateThresholdTestBed extends TestBed { actions: { diff --git a/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/watch_edit.helpers.ts b/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/watch_edit.helpers.ts index d0b458e30c70e..187f4dcaa0a76 100644 --- a/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/watch_edit.helpers.ts +++ b/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/watch_edit.helpers.ts @@ -4,10 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ import { registerTestBed, TestBed, TestBedConfig } from '../../../../../../test_utils'; -import { WatchEdit } from '../../../public/sections/watch_edit/components/watch_edit'; +import { WatchEdit } from '../../../public/np_ready/application/sections/watch_edit/components/watch_edit'; import { ROUTES } from '../../../common/constants'; -import { registerRouter } from '../../../public/lib/navigation'; +import { registerRouter } from '../../../public/np_ready/application/lib/navigation'; import { WATCH_ID } from './constants'; +import { withAppContext } from './app_context.mock'; const testBedConfig: TestBedConfig = { memoryRouter: { @@ -18,7 +19,7 @@ const testBedConfig: TestBedConfig = { doMountAsync: true, }; -const initTestBed = registerTestBed(WatchEdit, testBedConfig); +const initTestBed = registerTestBed(withAppContext(WatchEdit), testBedConfig); export interface WatchEditTestBed extends TestBed { actions: { diff --git a/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/watch_list.helpers.ts b/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/watch_list.helpers.ts index 0d3ecaa7a2b9a..e33327ea42ffe 100644 --- a/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/watch_list.helpers.ts +++ b/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/watch_list.helpers.ts @@ -13,8 +13,9 @@ import { TestBedConfig, nextTick, } from '../../../../../../test_utils'; -import { WatchList } from '../../../public/sections/watch_list/components/watch_list'; +import { WatchList } from '../../../public/np_ready/application/sections/watch_list/components/watch_list'; import { ROUTES } from '../../../common/constants'; +import { withAppContext } from './app_context.mock'; const testBedConfig: TestBedConfig = { memoryRouter: { @@ -23,7 +24,7 @@ const testBedConfig: TestBedConfig = { doMountAsync: true, }; -const initTestBed = registerTestBed(WatchList, testBedConfig); +const initTestBed = registerTestBed(withAppContext(WatchList), testBedConfig); export interface WatchListTestBed extends TestBed { actions: { diff --git a/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/watch_status.helpers.ts b/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/watch_status.helpers.ts index 22d57f255ebe6..e7bffe8924e31 100644 --- a/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/watch_status.helpers.ts +++ b/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/watch_status.helpers.ts @@ -13,9 +13,10 @@ import { TestBedConfig, nextTick, } from '../../../../../../test_utils'; -import { WatchStatus } from '../../../public/sections/watch_status/components/watch_status'; +import { WatchStatus } from '../../../public/np_ready/application/sections/watch_status/components/watch_status'; import { ROUTES } from '../../../common/constants'; import { WATCH_ID } from './constants'; +import { withAppContext } from './app_context.mock'; const testBedConfig: TestBedConfig = { memoryRouter: { @@ -25,7 +26,7 @@ const testBedConfig: TestBedConfig = { doMountAsync: true, }; -const initTestBed = registerTestBed(WatchStatus, testBedConfig); +const initTestBed = registerTestBed(withAppContext(WatchStatus), testBedConfig); export interface WatchStatusTestBed extends TestBed { actions: { diff --git a/x-pack/legacy/plugins/watcher/__jest__/client_integration/watch_create_json.test.ts b/x-pack/legacy/plugins/watcher/__jest__/client_integration/watch_create_json.test.ts index f45dbe156723b..4c893978ee5cb 100644 --- a/x-pack/legacy/plugins/watcher/__jest__/client_integration/watch_create_json.test.ts +++ b/x-pack/legacy/plugins/watcher/__jest__/client_integration/watch_create_json.test.ts @@ -4,22 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ import { act } from 'react-dom/test-utils'; -import { setupEnvironment, pageHelpers, nextTick } from './helpers'; +import { setupEnvironment, pageHelpers, nextTick, wrapBodyResponse } from './helpers'; import { WatchCreateJsonTestBed } from './helpers/watch_create_json.helpers'; import { WATCH } from './helpers/constants'; -import defaultWatchJson from '../../public/models/watch/default_watch.json'; +import defaultWatchJson from '../../public/np_ready/application/models/watch/default_watch.json'; import { getExecuteDetails } from '../../test/fixtures'; -jest.mock('ui/chrome', () => ({ - breadcrumbs: { set: () => {} }, - addBasePath: (path: string) => path || '/api/watcher', -})); - -jest.mock('ui/time_buckets', () => {}); - const { setup } = pageHelpers.watchCreateJson; -describe.skip(' create route', () => { +describe(' create route', () => { const { server, httpRequestsMockHelpers } = setupEnvironment(); let testBed: WatchCreateJsonTestBed; @@ -107,7 +100,7 @@ describe.skip(' create route', () => { 'There are {{ctx.payload.hits.total}} documents in your index. Threshold is 10.'; expect(latestRequest.requestBody).toEqual( - JSON.stringify({ + wrapBodyResponse({ id: watch.id, name: watch.name, type: watch.type, @@ -194,7 +187,7 @@ describe.skip(' create route', () => { }; expect(latestRequest.requestBody).toEqual( - JSON.stringify({ + wrapBodyResponse({ executeDetails: getExecuteDetails({ actionModes, }), @@ -258,7 +251,7 @@ describe.skip(' create route', () => { const scheduledTime = `now+${SCHEDULED_TIME}s`; expect(latestRequest.requestBody).toEqual( - JSON.stringify({ + wrapBodyResponse({ executeDetails: getExecuteDetails({ triggerData: { triggeredTime, diff --git a/x-pack/legacy/plugins/watcher/__jest__/client_integration/watch_create_threshold.test.tsx b/x-pack/legacy/plugins/watcher/__jest__/client_integration/watch_create_threshold.test.tsx index 62cfd92182091..36a5c150eead7 100644 --- a/x-pack/legacy/plugins/watcher/__jest__/client_integration/watch_create_threshold.test.tsx +++ b/x-pack/legacy/plugins/watcher/__jest__/client_integration/watch_create_threshold.test.tsx @@ -7,7 +7,13 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import axiosXhrAdapter from 'axios/lib/adapters/xhr'; import axios from 'axios'; -import { setupEnvironment, pageHelpers, nextTick } from './helpers'; +import { + setupEnvironment, + pageHelpers, + nextTick, + wrapBodyResponse, + unwrapBodyResponse, +} from './helpers'; import { WatchCreateThresholdTestBed } from './helpers/watch_create_threshold.helpers'; import { getExecuteDetails } from '../../test/fixtures'; import { WATCH_TYPES } from '../../common/constants'; @@ -42,31 +48,8 @@ const WATCH_VISUALIZE_DATA = { const mockHttpClient = axios.create({ adapter: axiosXhrAdapter }); -jest.mock('ui/chrome', () => ({ - breadcrumbs: { set: () => {} }, - addBasePath: (path: string) => path || '/api/watcher', - getUiSettingsClient: () => ({ - get: () => {}, - isDefault: () => true, - }), -})); - -jest.mock('ui/time_buckets', () => { - class MockTimeBuckets { - setBounds(_domain: any) { - return {}; - } - getInterval() { - return { - expression: {}, - }; - } - } - return { TimeBuckets: MockTimeBuckets }; -}); - -jest.mock('../../public/lib/api', () => ({ - ...jest.requireActual('../../public/lib/api'), +jest.mock('../../public/np_ready/application/lib/api', () => ({ + ...jest.requireActual('../../public/np_ready/application/lib/api'), loadIndexPatterns: async () => { const INDEX_PATTERNS = [ { attributes: { title: 'index1' } }, @@ -85,7 +68,7 @@ jest.mock('@elastic/eui', () => ({ EuiComboBox: (props: any) => ( { + onChange={(syntheticEvent: any) => { props.onChange([syntheticEvent['0']]); }} /> @@ -94,7 +77,7 @@ jest.mock('@elastic/eui', () => ({ const { setup } = pageHelpers.watchCreateThreshold; -describe.skip(' create route', () => { +describe(' create route', () => { const { server, httpRequestsMockHelpers } = setupEnvironment(); let testBed: WatchCreateThresholdTestBed; @@ -105,12 +88,9 @@ describe.skip(' create route', () => { describe('on component mount', () => { beforeEach(async () => { testBed = await setup(); - - await act(async () => { - const { component } = testBed; - await nextTick(); - component.update(); - }); + const { component } = testBed; + await nextTick(); + component.update(); }); test('should set the correct page title', () => { @@ -125,13 +105,6 @@ describe.skip(' create route', () => { httpRequestsMockHelpers.setLoadEsFieldsResponse({ fields: ES_FIELDS }); httpRequestsMockHelpers.setLoadSettingsResponse(SETTINGS); httpRequestsMockHelpers.setLoadWatchVisualizeResponse(WATCH_VISUALIZE_DATA); - - testBed = await setup(); - - await act(async () => { - await nextTick(); - testBed.component.update(); - }); }); describe('form validation', () => { @@ -173,7 +146,7 @@ describe.skip(' create route', () => { expect(find('saveWatchButton').props().disabled).toEqual(true); }); - test('it should enable the Create button and render additonal content with valid fields', async () => { + test('it should enable the Create button and render additional content with valid fields', async () => { const { form, find, component, exists } = testBed; form.setInputValue('nameInput', 'my_test_watch'); @@ -192,39 +165,30 @@ describe.skip(' create route', () => { expect(exists('watchActionsPanel')).toBe(true); }); - describe('watch conditions', () => { - beforeEach(async () => { - const { form, find, component } = testBed; + // Looks like there is an issue with using 'mockComboBox'. + describe.skip('watch conditions', () => { + beforeEach(() => { + const { form, find } = testBed; // Name, index and time fields are required before the watch condition expression renders form.setInputValue('nameInput', 'my_test_watch'); - find('mockComboBox').simulate('change', [{ label: 'index1', value: 'index1' }]); // Using mocked EuiComboBox - form.setInputValue('watchTimeFieldSelect', '@timestamp'); - - await act(async () => { - await nextTick(); - component.update(); + act(() => { + find('mockComboBox').simulate('change', [{ label: 'index1', value: 'index1' }]); // Using mocked EuiComboBox }); + form.setInputValue('watchTimeFieldSelect', '@timestamp'); }); - test('should require a threshold value', async () => { - const { form, find, component } = testBed; - - find('watchThresholdButton').simulate('click'); + test('should require a threshold value', () => { + const { form, find } = testBed; - // Provide invalid value - form.setInputValue('watchThresholdInput', ''); - - expect(form.getErrorsMessages()).toContain('A value is required.'); - - // Provide valid value - form.setInputValue('watchThresholdInput', '0'); - - await act(async () => { - await nextTick(); - component.update(); + act(() => { + find('watchThresholdButton').simulate('click'); + // Provide invalid value + form.setInputValue('watchThresholdInput', ''); + // Provide valid value + form.setInputValue('watchThresholdInput', '0'); }); - + expect(form.getErrorsMessages()).toContain('A value is required.'); expect(form.getErrorsMessages().length).toEqual(0); }); }); @@ -273,7 +237,7 @@ describe.skip(' create route', () => { const latestRequest = server.requests[server.requests.length - 1]; const thresholdWatch = { - id: JSON.parse(latestRequest.requestBody).watch.id, // watch ID is created dynamically + id: unwrapBodyResponse(latestRequest.requestBody).watch.id, // watch ID is created dynamically name: WATCH_NAME, type: WATCH_TYPES.THRESHOLD, isNew: true, @@ -300,7 +264,7 @@ describe.skip(' create route', () => { }; expect(latestRequest.requestBody).toEqual( - JSON.stringify({ + wrapBodyResponse({ executeDetails: getExecuteDetails({ actionModes: { logging_1: 'force_execute', @@ -341,7 +305,7 @@ describe.skip(' create route', () => { const latestRequest = server.requests[server.requests.length - 1]; const thresholdWatch = { - id: JSON.parse(latestRequest.requestBody).watch.id, // watch ID is created dynamically + id: unwrapBodyResponse(latestRequest.requestBody).watch.id, // watch ID is created dynamically name: WATCH_NAME, type: WATCH_TYPES.THRESHOLD, isNew: true, @@ -367,7 +331,7 @@ describe.skip(' create route', () => { }; expect(latestRequest.requestBody).toEqual( - JSON.stringify({ + wrapBodyResponse({ executeDetails: getExecuteDetails({ actionModes: { index_1: 'force_execute', @@ -401,7 +365,7 @@ describe.skip(' create route', () => { const latestRequest = server.requests[server.requests.length - 1]; const thresholdWatch = { - id: JSON.parse(latestRequest.requestBody).watch.id, // watch ID is created dynamically + id: unwrapBodyResponse(latestRequest.requestBody).watch.id, // watch ID is created dynamically name: WATCH_NAME, type: WATCH_TYPES.THRESHOLD, isNew: true, @@ -430,7 +394,7 @@ describe.skip(' create route', () => { }; expect(latestRequest.requestBody).toEqual( - JSON.stringify({ + wrapBodyResponse({ executeDetails: getExecuteDetails({ actionModes: { slack_1: 'force_execute', @@ -471,7 +435,7 @@ describe.skip(' create route', () => { const latestRequest = server.requests[server.requests.length - 1]; const thresholdWatch = { - id: JSON.parse(latestRequest.requestBody).watch.id, // watch ID is created dynamically + id: unwrapBodyResponse(latestRequest.requestBody).watch.id, // watch ID is created dynamically name: WATCH_NAME, type: WATCH_TYPES.THRESHOLD, isNew: true, @@ -504,7 +468,7 @@ describe.skip(' create route', () => { }; expect(latestRequest.requestBody).toEqual( - JSON.stringify({ + wrapBodyResponse({ executeDetails: getExecuteDetails({ actionModes: { email_1: 'force_execute', @@ -559,7 +523,7 @@ describe.skip(' create route', () => { const latestRequest = server.requests[server.requests.length - 1]; const thresholdWatch = { - id: JSON.parse(latestRequest.requestBody).watch.id, // watch ID is created dynamically + id: unwrapBodyResponse(latestRequest.requestBody).watch.id, // watch ID is created dynamically name: WATCH_NAME, type: WATCH_TYPES.THRESHOLD, isNew: true, @@ -594,7 +558,7 @@ describe.skip(' create route', () => { }; expect(latestRequest.requestBody).toEqual( - JSON.stringify({ + wrapBodyResponse({ executeDetails: getExecuteDetails({ actionModes: { webhook_1: 'force_execute', @@ -645,7 +609,7 @@ describe.skip(' create route', () => { const latestRequest = server.requests[server.requests.length - 1]; const thresholdWatch = { - id: JSON.parse(latestRequest.requestBody).watch.id, // watch ID is created dynamically + id: unwrapBodyResponse(latestRequest.requestBody).watch.id, // watch ID is created dynamically name: WATCH_NAME, type: WATCH_TYPES.THRESHOLD, isNew: true, @@ -682,7 +646,7 @@ describe.skip(' create route', () => { }; expect(latestRequest.requestBody).toEqual( - JSON.stringify({ + wrapBodyResponse({ executeDetails: getExecuteDetails({ actionModes: { jira_1: 'force_execute', @@ -723,7 +687,7 @@ describe.skip(' create route', () => { const latestRequest = server.requests[server.requests.length - 1]; const thresholdWatch = { - id: JSON.parse(latestRequest.requestBody).watch.id, // watch ID is created dynamically + id: unwrapBodyResponse(latestRequest.requestBody).watch.id, // watch ID is created dynamically name: WATCH_NAME, type: WATCH_TYPES.THRESHOLD, isNew: true, @@ -750,7 +714,7 @@ describe.skip(' create route', () => { }; expect(latestRequest.requestBody).toEqual( - JSON.stringify({ + wrapBodyResponse({ executeDetails: getExecuteDetails({ actionModes: { pagerduty_1: 'force_execute', @@ -784,7 +748,7 @@ describe.skip(' create route', () => { const latestRequest = server.requests[server.requests.length - 1]; const thresholdWatch = { - id: JSON.parse(latestRequest.requestBody).id, // watch ID is created dynamically + id: unwrapBodyResponse(latestRequest.requestBody).id, // watch ID is created dynamically name: WATCH_NAME, type: WATCH_TYPES.THRESHOLD, isNew: true, @@ -801,7 +765,7 @@ describe.skip(' create route', () => { threshold: 1000, }; - expect(latestRequest.requestBody).toEqual(JSON.stringify(thresholdWatch)); + expect(latestRequest.requestBody).toEqual(wrapBodyResponse(thresholdWatch)); }); }); }); diff --git a/x-pack/legacy/plugins/watcher/__jest__/client_integration/watch_edit.test.ts b/x-pack/legacy/plugins/watcher/__jest__/client_integration/watch_edit.test.ts index fb9ad934249e9..1eee3d3b7e6ee 100644 --- a/x-pack/legacy/plugins/watcher/__jest__/client_integration/watch_edit.test.ts +++ b/x-pack/legacy/plugins/watcher/__jest__/client_integration/watch_edit.test.ts @@ -6,36 +6,17 @@ import { act } from 'react-dom/test-utils'; import axiosXhrAdapter from 'axios/lib/adapters/xhr'; import axios from 'axios'; -import { setupEnvironment, pageHelpers, nextTick } from './helpers'; +import { setupEnvironment, pageHelpers, nextTick, wrapBodyResponse } from './helpers'; import { WatchEditTestBed } from './helpers/watch_edit.helpers'; import { WATCH } from './helpers/constants'; -import defaultWatchJson from '../../public/models/watch/default_watch.json'; +import defaultWatchJson from '../../public/np_ready/application/models/watch/default_watch.json'; import { getWatch } from '../../test/fixtures'; import { getRandomString } from '../../../../../test_utils'; const mockHttpClient = axios.create({ adapter: axiosXhrAdapter }); -jest.mock('ui/chrome', () => ({ - breadcrumbs: { set: () => {} }, - addBasePath: (path: string) => path || '/api/watcher', -})); - -jest.mock('ui/time_buckets', () => { - class MockTimeBuckets { - setBounds(_domain: any) { - return {}; - } - getInterval() { - return { - expression: {}, - }; - } - } - return { TimeBuckets: MockTimeBuckets }; -}); - -jest.mock('../../public/lib/api', () => ({ - ...jest.requireActual('../../public/lib/api'), +jest.mock('../../public/np_ready/application/lib/api', () => ({ + ...jest.requireActual('../../public/np_ready/application/lib/api'), loadIndexPatterns: async () => { const INDEX_PATTERNS = [ { attributes: { title: 'index1' } }, @@ -49,7 +30,7 @@ jest.mock('../../public/lib/api', () => ({ const { setup } = pageHelpers.watchEdit; -describe.skip('', () => { +describe('', () => { const { server, httpRequestsMockHelpers } = setupEnvironment(); let testBed: WatchEditTestBed; @@ -110,7 +91,7 @@ describe.skip('', () => { 'There are {{ctx.payload.hits.total}} documents in your index. Threshold is 10.'; expect(latestRequest.requestBody).toEqual( - JSON.stringify({ + wrapBodyResponse({ id: watch.id, name: EDITED_WATCH_NAME, type: watch.type, @@ -202,7 +183,7 @@ describe.skip('', () => { } = watch; expect(latestRequest.requestBody).toEqual( - JSON.stringify({ + wrapBodyResponse({ id, name: EDITED_WATCH_NAME, type, diff --git a/x-pack/legacy/plugins/watcher/__jest__/client_integration/watch_list.test.ts b/x-pack/legacy/plugins/watcher/__jest__/client_integration/watch_list.test.ts index bc2eadb7d9be9..a0327c6dfa1db 100644 --- a/x-pack/legacy/plugins/watcher/__jest__/client_integration/watch_list.test.ts +++ b/x-pack/legacy/plugins/watcher/__jest__/client_integration/watch_list.test.ts @@ -18,16 +18,9 @@ import { ROUTES } from '../../common/constants'; const { API_ROOT } = ROUTES; -jest.mock('ui/chrome', () => ({ - breadcrumbs: { set: () => {} }, - addBasePath: (path: string) => path || '/api/watcher', -})); - -jest.mock('ui/time_buckets', () => {}); - const { setup } = pageHelpers.watchList; -describe.skip('', () => { +describe('', () => { const { server, httpRequestsMockHelpers } = setupEnvironment(); let testBed: WatchListTestBed; diff --git a/x-pack/legacy/plugins/watcher/__jest__/client_integration/watch_status.test.ts b/x-pack/legacy/plugins/watcher/__jest__/client_integration/watch_status.test.ts index e12acd2e32ccf..973c14893f342 100644 --- a/x-pack/legacy/plugins/watcher/__jest__/client_integration/watch_status.test.ts +++ b/x-pack/legacy/plugins/watcher/__jest__/client_integration/watch_status.test.ts @@ -14,13 +14,6 @@ import { WATCH_STATES, ACTION_STATES } from '../../common/constants'; const { API_ROOT } = ROUTES; -jest.mock('ui/chrome', () => ({ - breadcrumbs: { set: () => {} }, - addBasePath: (path: string) => path || '/api/watcher', -})); - -jest.mock('ui/time_buckets', () => {}); - const { setup } = pageHelpers.watchStatus; const watchHistory1 = getWatchHistory({ startTime: '2019-06-04T01:11:11.294' }); @@ -45,7 +38,7 @@ const watch = { }, }; -describe.skip('', () => { +describe('', () => { const { server, httpRequestsMockHelpers } = setupEnvironment(); let testBed: WatchStatusTestBed; diff --git a/x-pack/legacy/plugins/watcher/kibana.json b/x-pack/legacy/plugins/watcher/kibana.json new file mode 100644 index 0000000000000..ccec8a1b77683 --- /dev/null +++ b/x-pack/legacy/plugins/watcher/kibana.json @@ -0,0 +1,9 @@ +{ + "id": "watcher", + "version": "kibana", + "requiredPlugins": [ + "home" + ], + "server": true, + "ui": true +} diff --git a/x-pack/legacy/plugins/watcher/plugin_definition.js b/x-pack/legacy/plugins/watcher/plugin_definition.js deleted file mode 100644 index 4a5946cc4974d..0000000000000 --- a/x-pack/legacy/plugins/watcher/plugin_definition.js +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { resolve } from 'path'; -import { i18n } from '@kbn/i18n'; -import { registerFieldsRoutes } from './server/routes/api/fields'; -import { registerSettingsRoutes } from './server/routes/api/settings'; -import { registerHistoryRoutes } from './server/routes/api/history'; -import { registerIndicesRoutes } from './server/routes/api/indices'; -import { registerLicenseRoutes } from './server/routes/api/license'; -import { registerWatchesRoutes } from './server/routes/api/watches'; -import { registerWatchRoutes } from './server/routes/api/watch'; -import { registerLicenseChecker } from '../../server/lib/register_license_checker'; -import { PLUGIN } from './common/constants'; - -export const pluginDefinition = { - id: PLUGIN.ID, - configPrefix: 'xpack.watcher', - publicDir: resolve(__dirname, 'public'), - require: ['kibana', 'elasticsearch', 'xpack_main'], - uiExports: { - styleSheetPaths: resolve(__dirname, 'public/index.scss'), - managementSections: ['plugins/watcher'], - home: ['plugins/watcher/register_feature'], - }, - init: function (server) { - // Register license checker - registerLicenseChecker( - server, - PLUGIN.ID, - PLUGIN.getI18nName(i18n), - PLUGIN.MINIMUM_LICENSE_REQUIRED - ); - - registerFieldsRoutes(server); - registerHistoryRoutes(server); - registerIndicesRoutes(server); - registerLicenseRoutes(server); - registerSettingsRoutes(server); - registerWatchesRoutes(server); - registerWatchRoutes(server); - }, -}; diff --git a/x-pack/legacy/plugins/watcher/plugin_definition.ts b/x-pack/legacy/plugins/watcher/plugin_definition.ts new file mode 100644 index 0000000000000..2da05253fdb32 --- /dev/null +++ b/x-pack/legacy/plugins/watcher/plugin_definition.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { resolve } from 'path'; +import { plugin } from './server/np_ready'; +import { PLUGIN } from './common/constants'; + +export const pluginDefinition = { + id: PLUGIN.ID, + configPrefix: 'xpack.watcher', + publicDir: resolve(__dirname, 'public'), + require: ['kibana', 'elasticsearch', 'xpack_main'], + uiExports: { + styleSheetPaths: resolve(__dirname, 'public/np_ready/application/index.scss'), + managementSections: ['plugins/watcher/legacy'], + home: ['plugins/watcher/register_feature'], + }, + init(server: any) { + plugin({} as any).setup(server.newPlatform.setup.core, { + __LEGACY: { + route: server.route.bind(server), + plugins: { + watcher: server.plugins[PLUGIN.ID], + xpack_main: server.plugins.xpack_main, + }, + }, + }); + }, +}; diff --git a/x-pack/legacy/plugins/watcher/public/app.html b/x-pack/legacy/plugins/watcher/public/app.html deleted file mode 100644 index 8c7c3eb946aef..0000000000000 --- a/x-pack/legacy/plugins/watcher/public/app.html +++ /dev/null @@ -1,3 +0,0 @@ - -
-
\ No newline at end of file diff --git a/x-pack/legacy/plugins/watcher/public/legacy.ts b/x-pack/legacy/plugins/watcher/public/legacy.ts new file mode 100644 index 0000000000000..d7b85ccfeb7b4 --- /dev/null +++ b/x-pack/legacy/plugins/watcher/public/legacy.ts @@ -0,0 +1,146 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CoreSetup, App, AppUnmount } from 'src/core/public'; +import { i18n } from '@kbn/i18n'; + +/* Legacy UI imports */ +import { npSetup, npStart } from 'ui/new_platform'; +import routes from 'ui/routes'; +import { management, MANAGEMENT_BREADCRUMB } from 'ui/management'; +import { TimeBuckets } from 'ui/time_buckets'; +// @ts-ignore +import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; +/* Legacy UI imports */ + +import { plugin } from './np_ready'; +import { PLUGIN } from '../common/constants'; +import { LICENSE_STATUS_INVALID, LICENSE_STATUS_UNAVAILABLE } from '../../../common/constants'; +import { manageAngularLifecycle } from './manage_angular_lifecycle'; + +const template = ` +
+
`; + +let elem: HTMLElement; +let mountApp: () => AppUnmount | Promise; +let unmountApp: AppUnmount | Promise; +routes.when('/management/elasticsearch/watcher/:param1?/:param2?/:param3?/:param4?', { + template, + controller: class WatcherController { + constructor($injector: any, $scope: any) { + const $route = $injector.get('$route'); + const licenseStatus = xpackInfo.get(`features.${PLUGIN.ID}`); + const shimCore: CoreSetup = { + ...npSetup.core, + application: { + ...npSetup.core.application, + register(app: App): void { + mountApp = () => + app.mount(npStart as any, { + element: elem, + appBasePath: '/management/elasticsearch/watcher/', + }); + }, + }, + }; + + // clean up previously rendered React app if one exists + // this happens because of React Router redirects + if (elem) { + ((unmountApp as unknown) as AppUnmount)(); + } + + $scope.$$postDigest(() => { + elem = document.getElementById('watchReactRoot')!; + const instance = plugin(); + instance.setup(shimCore, { + ...(npSetup.plugins as typeof npSetup.plugins & { eui_utils: any }), + __LEGACY: { + MANAGEMENT_BREADCRUMB, + TimeBuckets, + licenseStatus, + }, + }); + + instance.start(npStart.core, npStart.plugins); + + (mountApp() as Promise).then(fn => (unmountApp = fn)); + + manageAngularLifecycle($scope, $route, elem); + }); + } + } as any, + // @ts-ignore + controllerAs: 'watchRoute', +}); + +routes.defaults(/\/management/, { + resolve: { + watcherManagementSection: () => { + const watchesSection = management.getSection('elasticsearch/watcher'); + const licenseStatus = xpackInfo.get(`features.${PLUGIN.ID}`); + const { status } = licenseStatus; + + if (status === LICENSE_STATUS_INVALID || status === LICENSE_STATUS_UNAVAILABLE) { + return watchesSection.hide(); + } + + watchesSection.show(); + }, + }, +}); + +management.getSection('elasticsearch').register('watcher', { + display: i18n.translate('xpack.watcher.sections.watchList.managementSection.watcherDisplayName', { + defaultMessage: 'Watcher', + }), + order: 6, + url: '#/management/elasticsearch/watcher/', +} as any); + +management.getSection('elasticsearch/watcher').register('watches', { + display: i18n.translate('xpack.watcher.sections.watchList.managementSection.watchesDisplayName', { + defaultMessage: 'Watches', + }), + order: 1, +} as any); + +management.getSection('elasticsearch/watcher').register('watch', { + visible: false, +} as any); + +management.getSection('elasticsearch/watcher/watch').register('status', { + display: i18n.translate('xpack.watcher.sections.watchList.managementSection.statusDisplayName', { + defaultMessage: 'Status', + }), + order: 1, + visible: false, +} as any); + +management.getSection('elasticsearch/watcher/watch').register('edit', { + display: i18n.translate('xpack.watcher.sections.watchList.managementSection.editDisplayName', { + defaultMessage: 'Edit', + }), + order: 2, + visible: false, +} as any); + +management.getSection('elasticsearch/watcher/watch').register('new', { + display: i18n.translate( + 'xpack.watcher.sections.watchList.managementSection.newWatchDisplayName', + { + defaultMessage: 'New Watch', + } + ), + order: 1, + visible: false, +} as any); + +management.getSection('elasticsearch/watcher/watch').register('history-item', { + order: 1, + visible: false, +} as any); diff --git a/x-pack/legacy/plugins/watcher/public/lib/documentation_links/documentation_links.ts b/x-pack/legacy/plugins/watcher/public/lib/documentation_links/documentation_links.ts deleted file mode 100644 index 88f23465d33e8..0000000000000 --- a/x-pack/legacy/plugins/watcher/public/lib/documentation_links/documentation_links.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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } from 'ui/documentation_links'; -import { ACTION_TYPES } from '../../../common/constants'; - -const elasticDocLinkBase = `${ELASTIC_WEBSITE_URL}guide/en/`; - -const esBase = `${elasticDocLinkBase}elasticsearch/reference/${DOC_LINK_VERSION}`; -const esStackBase = `${elasticDocLinkBase}elastic-stack-overview/${DOC_LINK_VERSION}`; -const kibanaBase = `${elasticDocLinkBase}kibana/${DOC_LINK_VERSION}`; - -export const putWatchApiUrl = `${esBase}/watcher-api-put-watch.html`; -export const executeWatchApiUrl = `${esBase}/watcher-api-execute-watch.html#watcher-api-execute-watch-action-mode`; -export const watcherGettingStartedUrl = `${kibanaBase}/watcher-ui.html`; - -export const watchActionsConfigurationMap = { - [ACTION_TYPES.SLACK]: `${esStackBase}/actions-slack.html#configuring-slack`, - [ACTION_TYPES.PAGERDUTY]: `${esStackBase}/actions-pagerduty.html#configuring-pagerduty`, - [ACTION_TYPES.JIRA]: `${esStackBase}/actions-jira.html#configuring-jira`, -}; diff --git a/x-pack/legacy/plugins/watcher/public/lib/manage_angular_lifecycle.js b/x-pack/legacy/plugins/watcher/public/manage_angular_lifecycle.ts similarity index 75% rename from x-pack/legacy/plugins/watcher/public/lib/manage_angular_lifecycle.js rename to x-pack/legacy/plugins/watcher/public/manage_angular_lifecycle.ts index 3813e632a0a73..efd40eaf83daa 100644 --- a/x-pack/legacy/plugins/watcher/public/lib/manage_angular_lifecycle.js +++ b/x-pack/legacy/plugins/watcher/public/manage_angular_lifecycle.ts @@ -6,7 +6,7 @@ import { unmountComponentAtNode } from 'react-dom'; -export const manageAngularLifecycle = ($scope, $route, elem) => { +export const manageAngularLifecycle = ($scope: any, $route: any, elem: HTMLElement) => { const lastRoute = $route.current; const deregister = $scope.$on('$locationChangeSuccess', () => { @@ -17,7 +17,12 @@ export const manageAngularLifecycle = ($scope, $route, elem) => { }); $scope.$on('$destroy', () => { - deregister && deregister(); - elem && unmountComponentAtNode(elem); + if (deregister) { + deregister(); + } + + if (elem) { + unmountComponentAtNode(elem); + } }); }; diff --git a/x-pack/legacy/plugins/watcher/public/models/index.d.ts b/x-pack/legacy/plugins/watcher/public/models/index.d.ts deleted file mode 100644 index d96d8d192e166..0000000000000 --- a/x-pack/legacy/plugins/watcher/public/models/index.d.ts +++ /dev/null @@ -1,42 +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; - * you may not use this file except in compliance with the Elastic License. - */ -declare module 'plugins/watcher/models/visualize_options' { - export const VisualizeOptions: any; -} - -declare module 'plugins/watcher/models/watch' { - export const Watch: any; -} - -declare module 'plugins/watcher/models/watch/threshold_watch' { - export const ThresholdWatch: any; -} - -declare module 'plugins/watcher/models/watch/json_watch' { - export const JsonWatch: any; -} - -declare module 'plugins/watcher/models/execute_details/execute_details' { - export const ExecuteDetails: any; -} - -declare module 'plugins/watcher/models/watch_history_item' { - export const WatchHistoryItem: any; -} - -declare module 'plugins/watcher/models/watch_status' { - export const WatchStatus: any; -} - -declare module 'plugins/watcher/models/settings' { - export const Settings: any; -} -declare module 'plugins/watcher/models/action' { - export const Action: any; -} -declare module 'ui/time_buckets' { - export const TimeBuckets: any; -} diff --git a/x-pack/legacy/plugins/watcher/public/app.js b/x-pack/legacy/plugins/watcher/public/np_ready/application/app.tsx similarity index 60% rename from x-pack/legacy/plugins/watcher/public/app.js rename to x-pack/legacy/plugins/watcher/public/np_ready/application/app.tsx index b206348547966..36fa1cce9d6dd 100644 --- a/x-pack/legacy/plugins/watcher/public/app.js +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/app.tsx @@ -4,54 +4,61 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import { HashRouter, Switch, Route, Redirect } from 'react-router-dom'; +import React from 'react'; +import { + ChromeStart, + DocLinksStart, + HttpSetup, + ToastsSetup, + IUiSettingsClient, +} from 'src/core/public'; + +import { EuiCallOut, EuiLink } from '@elastic/eui'; +import { + HashRouter, + Switch, + Route, + Redirect, + withRouter, + RouteComponentProps, +} from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n/react'; import { WatchStatus } from './sections/watch_status/components/watch_status'; import { WatchEdit } from './sections/watch_edit/components/watch_edit'; import { WatchList } from './sections/watch_list/components/watch_list'; import { registerRouter } from './lib/navigation'; import { BASE_PATH } from './constants'; -import { LICENSE_STATUS_VALID } from '../../../common/constants'; -import { EuiCallOut, EuiLink } from '@elastic/eui'; +import { LICENSE_STATUS_VALID } from '../../../../../common/constants'; +import { AppContextProvider } from './app_context'; +import { LegacyDependencies } from '../types'; -class ShareRouter extends Component { - static contextTypes = { - router: PropTypes.shape({ - history: PropTypes.shape({ - push: PropTypes.func.isRequired, - createHref: PropTypes.func.isRequired - }).isRequired - }).isRequired - } - constructor(...args) { - super(...args); - this.registerRouter(); - } +const ShareRouter = withRouter(({ children, history }: RouteComponentProps & { children: any }) => { + registerRouter({ history }); + return children; +}); - registerRouter() { - // Share the router with the app without requiring React or context. - const { router } = this.context; - registerRouter(router); - } - - render() { - return this.props.children; - } +export interface AppDeps { + chrome: ChromeStart; + docLinks: DocLinksStart; + toasts: ToastsSetup; + http: HttpSetup; + uiSettings: IUiSettingsClient; + legacy: LegacyDependencies; + euiUtils: any; } -export const App = ({ licenseStatus }) => { - const { status, message } = licenseStatus; + +export const App = (deps: AppDeps) => { + const { status, message } = deps.legacy.licenseStatus; if (status !== LICENSE_STATUS_VALID) { return ( - )} + } color="warning" iconType="help" > @@ -69,7 +76,9 @@ export const App = ({ licenseStatus }) => { return ( - + + + ); @@ -81,7 +90,11 @@ export const AppWithoutRouter = () => ( - + ); diff --git a/x-pack/legacy/plugins/watcher/public/np_ready/application/app_context.tsx b/x-pack/legacy/plugins/watcher/public/np_ready/application/app_context.tsx new file mode 100644 index 0000000000000..5696ab3cb91ba --- /dev/null +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/app_context.tsx @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { createContext, useContext } from 'react'; +import { DocLinksStart } from 'src/core/public'; +import { ACTION_TYPES } from '../../../common/constants'; +import { AppDeps } from './app'; + +interface ContextValue extends Omit { + links: ReturnType; +} + +const AppContext = createContext(null as any); + +const generateDocLinks = ({ ELASTIC_WEBSITE_URL, DOC_LINK_VERSION }: DocLinksStart) => { + const elasticDocLinkBase = `${ELASTIC_WEBSITE_URL}guide/en/`; + const esBase = `${elasticDocLinkBase}elasticsearch/reference/${DOC_LINK_VERSION}`; + const kibanaBase = `${elasticDocLinkBase}kibana/${DOC_LINK_VERSION}`; + const putWatchApiUrl = `${esBase}/watcher-api-put-watch.html`; + const executeWatchApiUrl = `${esBase}/watcher-api-execute-watch.html#watcher-api-execute-watch-action-mode`; + const watcherGettingStartedUrl = `${kibanaBase}/watcher-ui.html`; + const watchActionsConfigurationMap = { + [ACTION_TYPES.SLACK]: `${esBase}/actions-slack.html#configuring-slack`, + [ACTION_TYPES.PAGERDUTY]: `${esBase}/actions-pagerduty.html#configuring-pagerduty`, + [ACTION_TYPES.JIRA]: `${esBase}/actions-jira.html#configuring-jira`, + }; + + return { + putWatchApiUrl, + executeWatchApiUrl, + watcherGettingStartedUrl, + watchActionsConfigurationMap, + }; +}; + +export const AppContextProvider = ({ + children, + value, +}: { + value: AppDeps; + children: React.ReactNode; +}) => { + const { docLinks, ...rest } = value; + return ( + + {children} + + ); +}; + +export const useAppContext = () => { + const ctx = useContext(AppContext); + if (!ctx) { + throw new Error('"useAppContext" can only be called inside of AppContext.Provider!'); + } + return ctx; +}; diff --git a/x-pack/legacy/plugins/watcher/public/np_ready/application/boot.tsx b/x-pack/legacy/plugins/watcher/public/np_ready/application/boot.tsx new file mode 100644 index 0000000000000..3f2a10f004649 --- /dev/null +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/boot.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; +import { SavedObjectsClientContract } from 'src/core/public'; + +import { App, AppDeps } from './app'; +import { setHttpClient, setSavedObjectsClient } from './lib/api'; +import { LegacyDependencies } from '../types'; + +interface BootDeps extends AppDeps { + element: HTMLElement; + savedObjects: SavedObjectsClientContract; + I18nContext: any; + legacy: LegacyDependencies; +} + +export const boot = (bootDeps: BootDeps) => { + const { I18nContext, element, legacy, savedObjects, ...appDeps } = bootDeps; + + setHttpClient(appDeps.http); + setSavedObjectsClient(savedObjects); + + render( + + + , + element + ); + return () => unmountComponentAtNode(element); +}; diff --git a/x-pack/legacy/plugins/watcher/public/components/confirm_watches_modal.tsx b/x-pack/legacy/plugins/watcher/public/np_ready/application/components/confirm_watches_modal.tsx similarity index 100% rename from x-pack/legacy/plugins/watcher/public/components/confirm_watches_modal.tsx rename to x-pack/legacy/plugins/watcher/public/np_ready/application/components/confirm_watches_modal.tsx diff --git a/x-pack/legacy/plugins/watcher/public/components/delete_watches_modal.tsx b/x-pack/legacy/plugins/watcher/public/np_ready/application/components/delete_watches_modal.tsx similarity index 95% rename from x-pack/legacy/plugins/watcher/public/components/delete_watches_modal.tsx rename to x-pack/legacy/plugins/watcher/public/np_ready/application/components/delete_watches_modal.tsx index 6d75495cbfc20..363185f3457d8 100644 --- a/x-pack/legacy/plugins/watcher/public/components/delete_watches_modal.tsx +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/components/delete_watches_modal.tsx @@ -6,8 +6,8 @@ import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; -import { toastNotifications } from 'ui/notify'; import { deleteWatches } from '../lib/api'; +import { useAppContext } from '../app_context'; export const DeleteWatchesModal = ({ watchesToDelete, @@ -16,6 +16,7 @@ export const DeleteWatchesModal = ({ watchesToDelete: string[]; callback: (deleted?: string[]) => void; }) => { + const { toasts } = useAppContext(); const numWatchesToDelete = watchesToDelete.length; if (!numWatchesToDelete) { return null; @@ -54,7 +55,7 @@ export const DeleteWatchesModal = ({ const numErrors = errors.length; callback(successes); if (numSuccesses > 0) { - toastNotifications.addSuccess( + toasts.addSuccess( i18n.translate( 'xpack.watcher.sections.watchList.deleteSelectedWatchesSuccessNotification.descriptionText', { @@ -67,7 +68,7 @@ export const DeleteWatchesModal = ({ } if (numErrors > 0) { - toastNotifications.addDanger( + toasts.addDanger( i18n.translate( 'xpack.watcher.sections.watchList.deleteSelectedWatchesErrorNotification.descriptionText', { diff --git a/x-pack/legacy/plugins/watcher/public/components/form_errors.tsx b/x-pack/legacy/plugins/watcher/public/np_ready/application/components/form_errors.tsx similarity index 100% rename from x-pack/legacy/plugins/watcher/public/components/form_errors.tsx rename to x-pack/legacy/plugins/watcher/public/np_ready/application/components/form_errors.tsx diff --git a/x-pack/legacy/plugins/watcher/public/components/index.ts b/x-pack/legacy/plugins/watcher/public/np_ready/application/components/index.ts similarity index 100% rename from x-pack/legacy/plugins/watcher/public/components/index.ts rename to x-pack/legacy/plugins/watcher/public/np_ready/application/components/index.ts diff --git a/x-pack/legacy/plugins/watcher/public/components/page_error/index.ts b/x-pack/legacy/plugins/watcher/public/np_ready/application/components/page_error/index.ts similarity index 100% rename from x-pack/legacy/plugins/watcher/public/components/page_error/index.ts rename to x-pack/legacy/plugins/watcher/public/np_ready/application/components/page_error/index.ts diff --git a/x-pack/legacy/plugins/watcher/public/components/page_error/page_error.tsx b/x-pack/legacy/plugins/watcher/public/np_ready/application/components/page_error/page_error.tsx similarity index 100% rename from x-pack/legacy/plugins/watcher/public/components/page_error/page_error.tsx rename to x-pack/legacy/plugins/watcher/public/np_ready/application/components/page_error/page_error.tsx diff --git a/x-pack/legacy/plugins/watcher/public/components/page_error/page_error_forbidden.tsx b/x-pack/legacy/plugins/watcher/public/np_ready/application/components/page_error/page_error_forbidden.tsx similarity index 100% rename from x-pack/legacy/plugins/watcher/public/components/page_error/page_error_forbidden.tsx rename to x-pack/legacy/plugins/watcher/public/np_ready/application/components/page_error/page_error_forbidden.tsx diff --git a/x-pack/legacy/plugins/watcher/public/components/page_error/page_error_not_exist.tsx b/x-pack/legacy/plugins/watcher/public/np_ready/application/components/page_error/page_error_not_exist.tsx similarity index 100% rename from x-pack/legacy/plugins/watcher/public/components/page_error/page_error_not_exist.tsx rename to x-pack/legacy/plugins/watcher/public/np_ready/application/components/page_error/page_error_not_exist.tsx diff --git a/x-pack/legacy/plugins/watcher/public/components/section_error.tsx b/x-pack/legacy/plugins/watcher/public/np_ready/application/components/section_error.tsx similarity index 80% rename from x-pack/legacy/plugins/watcher/public/components/section_error.tsx rename to x-pack/legacy/plugins/watcher/public/np_ready/application/components/section_error.tsx index 8951b95b75078..1c77cf2b49ae2 100644 --- a/x-pack/legacy/plugins/watcher/public/components/section_error.tsx +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/components/section_error.tsx @@ -8,6 +8,18 @@ import { EuiCallOut, EuiSpacer } from '@elastic/eui'; import React, { Fragment } from 'react'; export interface Error { + error: string; + + /** + * wrapEsError() on the server adds a "cause" array + */ + cause?: string[]; + + message?: string; + + /** + * @deprecated + */ data: { error: string; cause?: string[]; @@ -21,11 +33,9 @@ interface Props { } export const SectionError: React.FunctionComponent = ({ title, error, ...rest }) => { - const { - error: errorString, - cause, // wrapEsError() on the server adds a "cause" array - message, - } = error.data; + const data = error.data || error; + + const { error: errorString, cause, message } = data; return ( diff --git a/x-pack/legacy/plugins/watcher/public/components/section_loading.tsx b/x-pack/legacy/plugins/watcher/public/np_ready/application/components/section_loading.tsx similarity index 100% rename from x-pack/legacy/plugins/watcher/public/components/section_loading.tsx rename to x-pack/legacy/plugins/watcher/public/np_ready/application/components/section_loading.tsx diff --git a/x-pack/legacy/plugins/watcher/public/components/watch_status.tsx b/x-pack/legacy/plugins/watcher/public/np_ready/application/components/watch_status.tsx similarity index 95% rename from x-pack/legacy/plugins/watcher/public/components/watch_status.tsx rename to x-pack/legacy/plugins/watcher/public/np_ready/application/components/watch_status.tsx index 39e6a5247b4a6..8afd174f8561e 100644 --- a/x-pack/legacy/plugins/watcher/public/components/watch_status.tsx +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/components/watch_status.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { EuiIcon, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; -import { ACTION_STATES, WATCH_STATES } from '../../common/constants'; +import { ACTION_STATES, WATCH_STATES } from '../../../../common/constants'; function StatusIcon({ status }: { status: string }) { switch (status) { diff --git a/x-pack/legacy/plugins/watcher/public/constants/base_path.ts b/x-pack/legacy/plugins/watcher/public/np_ready/application/constants/base_path.ts similarity index 100% rename from x-pack/legacy/plugins/watcher/public/constants/base_path.ts rename to x-pack/legacy/plugins/watcher/public/np_ready/application/constants/base_path.ts diff --git a/x-pack/legacy/plugins/watcher/public/constants/index.ts b/x-pack/legacy/plugins/watcher/public/np_ready/application/constants/index.ts similarity index 100% rename from x-pack/legacy/plugins/watcher/public/constants/index.ts rename to x-pack/legacy/plugins/watcher/public/np_ready/application/constants/index.ts diff --git a/x-pack/legacy/plugins/watcher/public/index.scss b/x-pack/legacy/plugins/watcher/public/np_ready/application/index.scss similarity index 100% rename from x-pack/legacy/plugins/watcher/public/index.scss rename to x-pack/legacy/plugins/watcher/public/np_ready/application/index.scss diff --git a/x-pack/legacy/plugins/watcher/public/lib/api.ts b/x-pack/legacy/plugins/watcher/public/np_ready/application/lib/api.ts similarity index 61% rename from x-pack/legacy/plugins/watcher/public/lib/api.ts rename to x-pack/legacy/plugins/watcher/public/np_ready/application/lib/api.ts index d5c430f9244c4..c08545904e351 100644 --- a/x-pack/legacy/plugins/watcher/public/lib/api.ts +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/lib/api.ts @@ -3,20 +3,20 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { Settings } from 'plugins/watcher/models/settings'; -import { Watch } from 'plugins/watcher/models/watch'; -import { WatchHistoryItem } from 'plugins/watcher/models/watch_history_item'; -import { WatchStatus } from 'plugins/watcher/models/watch_status'; - -import { __await } from 'tslib'; -import chrome from 'ui/chrome'; -import { ROUTES } from '../../common/constants'; -import { BaseWatch, ExecutedWatchDetails } from '../../common/types/watch_types'; +import { HttpSetup, SavedObjectsClientContract } from 'src/core/public'; +import { Settings } from 'plugins/watcher/np_ready/application/models/settings'; +import { Watch } from 'plugins/watcher/np_ready/application/models/watch'; +import { WatchHistoryItem } from 'plugins/watcher/np_ready/application/models/watch_history_item'; +import { WatchStatus } from 'plugins/watcher/np_ready/application/models/watch_status'; + +import { BaseWatch, ExecutedWatchDetails } from '../../../../common/types/watch_types'; import { useRequest, sendRequest } from './use_request'; -let httpClient: ng.IHttpService; +import { ROUTES } from '../../../../common/constants'; + +let httpClient: HttpSetup; -export const setHttpClient = (anHttpClient: ng.IHttpService) => { +export const setHttpClient = (anHttpClient: HttpSetup) => { httpClient = anHttpClient; }; @@ -24,19 +24,17 @@ export const getHttpClient = () => { return httpClient; }; -let savedObjectsClient: any; +let savedObjectsClient: SavedObjectsClientContract; -export const setSavedObjectsClient = (aSavedObjectsClient: any) => { +export const setSavedObjectsClient = (aSavedObjectsClient: SavedObjectsClientContract) => { savedObjectsClient = aSavedObjectsClient; }; -export const getSavedObjectsClient = () => { - return savedObjectsClient; -}; +export const getSavedObjectsClient = () => savedObjectsClient; -const basePath = chrome.addBasePath(ROUTES.API_ROOT); +const basePath = ROUTES.API_ROOT; -export const loadWatches = (pollIntervalMs: number) => { +export const useLoadWatches = (pollIntervalMs: number) => { return useRequest({ path: `${basePath}/watches`, method: 'get', @@ -47,7 +45,7 @@ export const loadWatches = (pollIntervalMs: number) => { }); }; -export const loadWatchDetail = (id: string) => { +export const useLoadWatchDetail = (id: string) => { return useRequest({ path: `${basePath}/watch/${id}`, method: 'get', @@ -55,15 +53,10 @@ export const loadWatchDetail = (id: string) => { }); }; -export const loadWatchHistory = (id: string, startTime: string) => { - let path = `${basePath}/watch/${id}/history`; - - if (startTime) { - path += `?startTime=${startTime}`; - } - +export const useLoadWatchHistory = (id: string, startTime: string) => { return useRequest({ - path, + query: startTime ? { startTime } : undefined, + path: `${basePath}/watch/${id}/history`, method: 'get', deserializer: ({ watchHistoryItems = [] }: { watchHistoryItems: any }) => { return watchHistoryItems.map((historyItem: any) => @@ -73,7 +66,7 @@ export const loadWatchHistory = (id: string, startTime: string) => { }); }; -export const loadWatchHistoryDetail = (id: string | undefined) => { +export const useLoadWatchHistoryDetail = (id: string | undefined) => { return useRequest({ path: !id ? '' : `${basePath}/history/${id}`, method: 'get', @@ -83,12 +76,10 @@ export const loadWatchHistoryDetail = (id: string | undefined) => { }; export const deleteWatches = async (watchIds: string[]) => { - const body = { + const body = JSON.stringify({ watchIds, - }; - const { - data: { results }, - } = await getHttpClient().post(`${basePath}/watches/delete`, body); + }); + const { results } = await getHttpClient().post(`${basePath}/watches/delete`, { body }); return results; }; @@ -107,8 +98,8 @@ export const activateWatch = async (id: string) => { }; export const loadWatch = async (id: string) => { - const { data: watch } = await getHttpClient().get(`${basePath}/watch/${id}`); - return Watch.fromUpstreamJson(watch.watch); + const { watch } = await getHttpClient().get(`${basePath}/watch/${id}`); + return Watch.fromUpstreamJson(watch); }; export const getMatchingIndices = async (pattern: string) => { @@ -118,32 +109,32 @@ export const getMatchingIndices = async (pattern: string) => { if (!pattern.endsWith('*')) { pattern = `${pattern}*`; } - const { - data: { indices }, - } = await getHttpClient().post(`${basePath}/indices`, { pattern }); + const body = JSON.stringify({ pattern }); + const { indices } = await getHttpClient().post(`${basePath}/indices`, { body }); return indices; }; export const fetchFields = async (indexes: string[]) => { - const { - data: { fields }, - } = await getHttpClient().post(`${basePath}/fields`, { indexes }); + const { fields } = await getHttpClient().post(`${basePath}/fields`, { + body: JSON.stringify({ indexes }), + }); return fields; }; export const createWatch = async (watch: BaseWatch) => { - const { data } = await getHttpClient().put(`${basePath}/watch/${watch.id}`, watch.upstreamJson); - return data; + return await getHttpClient().put(`${basePath}/watch/${watch.id}`, { + body: JSON.stringify(watch.upstreamJson), + }); }; export const executeWatch = async (executeWatchDetails: ExecutedWatchDetails, watch: BaseWatch) => { return sendRequest({ path: `${basePath}/watch/execute`, method: 'put', - body: { + body: JSON.stringify({ executeDetails: executeWatchDetails.upstreamJson, watch: watch.upstreamJson, - }, + }), }); }; @@ -156,19 +147,19 @@ export const loadIndexPatterns = async () => { return savedObjects; }; -export const getWatchVisualizationData = (watchModel: BaseWatch, visualizeOptions: any) => { +export const useGetWatchVisualizationData = (watchModel: BaseWatch, visualizeOptions: any) => { return useRequest({ path: `${basePath}/watch/visualize`, method: 'post', - body: { + body: JSON.stringify({ watch: watchModel.upstreamJson, options: visualizeOptions.upstreamJson, - }, + }), deserializer: ({ visualizeData }: { visualizeData: any }) => visualizeData, }); }; -export const loadSettings = () => { +export const useLoadSettings = () => { return useRequest({ path: `${basePath}/settings`, method: 'get', @@ -183,11 +174,8 @@ export const loadSettings = () => { }; export const ackWatchAction = async (watchId: string, actionId: string) => { - const { - data: { watchStatus }, - } = await getHttpClient().put( - `${basePath}/watch/${watchId}/action/${actionId}/acknowledge`, - null + const { watchStatus } = await getHttpClient().put( + `${basePath}/watch/${watchId}/action/${actionId}/acknowledge` ); return WatchStatus.fromUpstreamJson(watchStatus); }; diff --git a/x-pack/legacy/plugins/watcher/public/lib/breadcrumbs.ts b/x-pack/legacy/plugins/watcher/public/np_ready/application/lib/breadcrumbs.ts similarity index 100% rename from x-pack/legacy/plugins/watcher/public/lib/breadcrumbs.ts rename to x-pack/legacy/plugins/watcher/public/np_ready/application/lib/breadcrumbs.ts diff --git a/x-pack/legacy/plugins/watcher/public/lib/format_date.ts b/x-pack/legacy/plugins/watcher/public/np_ready/application/lib/format_date.ts similarity index 100% rename from x-pack/legacy/plugins/watcher/public/lib/format_date.ts rename to x-pack/legacy/plugins/watcher/public/np_ready/application/lib/format_date.ts diff --git a/x-pack/legacy/plugins/watcher/public/lib/get_search_value.ts b/x-pack/legacy/plugins/watcher/public/np_ready/application/lib/get_search_value.ts similarity index 100% rename from x-pack/legacy/plugins/watcher/public/lib/get_search_value.ts rename to x-pack/legacy/plugins/watcher/public/np_ready/application/lib/get_search_value.ts diff --git a/x-pack/legacy/plugins/watcher/public/lib/get_time_unit_label.ts b/x-pack/legacy/plugins/watcher/public/np_ready/application/lib/get_time_unit_label.ts similarity index 95% rename from x-pack/legacy/plugins/watcher/public/lib/get_time_unit_label.ts rename to x-pack/legacy/plugins/watcher/public/np_ready/application/lib/get_time_unit_label.ts index 35bd19e7007c6..ce3b96ac17def 100644 --- a/x-pack/legacy/plugins/watcher/public/lib/get_time_unit_label.ts +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/lib/get_time_unit_label.ts @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import { TIME_UNITS } from '../../common/constants'; +import { TIME_UNITS } from '../../../../common/constants'; export function getTimeUnitLabel(timeUnit = TIME_UNITS.SECOND, timeValue = '0') { switch (timeUnit) { diff --git a/x-pack/legacy/plugins/watcher/public/lib/navigation.ts b/x-pack/legacy/plugins/watcher/public/np_ready/application/lib/navigation.ts similarity index 100% rename from x-pack/legacy/plugins/watcher/public/lib/navigation.ts rename to x-pack/legacy/plugins/watcher/public/np_ready/application/lib/navigation.ts diff --git a/x-pack/legacy/plugins/watcher/public/lib/use_request.ts b/x-pack/legacy/plugins/watcher/public/np_ready/application/lib/use_request.ts similarity index 99% rename from x-pack/legacy/plugins/watcher/public/lib/use_request.ts rename to x-pack/legacy/plugins/watcher/public/np_ready/application/lib/use_request.ts index 4788b655d9e88..572403b14b9df 100644 --- a/x-pack/legacy/plugins/watcher/public/lib/use_request.ts +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/lib/use_request.ts @@ -11,6 +11,7 @@ import { sendRequest as _sendRequest, useRequest as _useRequest, } from '../shared_imports'; + import { getHttpClient } from './api'; export const sendRequest = (config: SendRequestConfig): Promise => { diff --git a/x-pack/legacy/plugins/watcher/public/models/action/action.js b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/action/action.js similarity index 95% rename from x-pack/legacy/plugins/watcher/public/models/action/action.js rename to x-pack/legacy/plugins/watcher/public/np_ready/application/models/action/action.js index 2f1850c3a434c..4e6ec21703b96 100644 --- a/x-pack/legacy/plugins/watcher/public/models/action/action.js +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/action/action.js @@ -5,7 +5,7 @@ */ import { get, set } from 'lodash'; -import { ACTION_TYPES } from '../../../common/constants'; +import { ACTION_TYPES } from '../../../../../common/constants'; import { EmailAction } from './email_action'; import { LoggingAction } from './logging_action'; import { SlackAction } from './slack_action'; diff --git a/x-pack/legacy/plugins/watcher/public/models/action/base_action.js b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/action/base_action.js similarity index 100% rename from x-pack/legacy/plugins/watcher/public/models/action/base_action.js rename to x-pack/legacy/plugins/watcher/public/np_ready/application/models/action/base_action.js diff --git a/x-pack/legacy/plugins/watcher/public/models/action/email_action.js b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/action/email_action.js similarity index 100% rename from x-pack/legacy/plugins/watcher/public/models/action/email_action.js rename to x-pack/legacy/plugins/watcher/public/np_ready/application/models/action/email_action.js diff --git a/x-pack/legacy/plugins/watcher/public/models/action/index.js b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/action/index.js similarity index 100% rename from x-pack/legacy/plugins/watcher/public/models/action/index.js rename to x-pack/legacy/plugins/watcher/public/np_ready/application/models/action/index.js diff --git a/x-pack/legacy/plugins/watcher/public/models/action/index_action.js b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/action/index_action.js similarity index 100% rename from x-pack/legacy/plugins/watcher/public/models/action/index_action.js rename to x-pack/legacy/plugins/watcher/public/np_ready/application/models/action/index_action.js diff --git a/x-pack/legacy/plugins/watcher/public/models/action/jira_action.js b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/action/jira_action.js similarity index 100% rename from x-pack/legacy/plugins/watcher/public/models/action/jira_action.js rename to x-pack/legacy/plugins/watcher/public/np_ready/application/models/action/jira_action.js diff --git a/x-pack/legacy/plugins/watcher/public/models/action/logging_action.js b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/action/logging_action.js similarity index 100% rename from x-pack/legacy/plugins/watcher/public/models/action/logging_action.js rename to x-pack/legacy/plugins/watcher/public/np_ready/application/models/action/logging_action.js diff --git a/x-pack/legacy/plugins/watcher/public/models/action/pagerduty_action.js b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/action/pagerduty_action.js similarity index 100% rename from x-pack/legacy/plugins/watcher/public/models/action/pagerduty_action.js rename to x-pack/legacy/plugins/watcher/public/np_ready/application/models/action/pagerduty_action.js diff --git a/x-pack/legacy/plugins/watcher/public/models/action/slack_action.js b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/action/slack_action.js similarity index 100% rename from x-pack/legacy/plugins/watcher/public/models/action/slack_action.js rename to x-pack/legacy/plugins/watcher/public/np_ready/application/models/action/slack_action.js diff --git a/x-pack/legacy/plugins/watcher/public/models/action/unknown_action.js b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/action/unknown_action.js similarity index 100% rename from x-pack/legacy/plugins/watcher/public/models/action/unknown_action.js rename to x-pack/legacy/plugins/watcher/public/np_ready/application/models/action/unknown_action.js diff --git a/x-pack/legacy/plugins/watcher/public/models/action/webhook_action.js b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/action/webhook_action.js similarity index 100% rename from x-pack/legacy/plugins/watcher/public/models/action/webhook_action.js rename to x-pack/legacy/plugins/watcher/public/np_ready/application/models/action/webhook_action.js diff --git a/x-pack/legacy/plugins/watcher/public/models/action_status/action_status.js b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/action_status/action_status.js similarity index 95% rename from x-pack/legacy/plugins/watcher/public/models/action_status/action_status.js rename to x-pack/legacy/plugins/watcher/public/np_ready/application/models/action_status/action_status.js index fa9e056554ab0..b177eb5bb2291 100644 --- a/x-pack/legacy/plugins/watcher/public/models/action_status/action_status.js +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/action_status/action_status.js @@ -5,7 +5,7 @@ */ import { get } from 'lodash'; -import { getMoment } from '../../../common/lib/get_moment'; +import { getMoment } from '../../../../../common/lib/get_moment'; export class ActionStatus { constructor(props = {}) { diff --git a/x-pack/legacy/plugins/watcher/public/models/action_status/index.js b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/action_status/index.js similarity index 100% rename from x-pack/legacy/plugins/watcher/public/models/action_status/index.js rename to x-pack/legacy/plugins/watcher/public/np_ready/application/models/action_status/index.js diff --git a/x-pack/legacy/plugins/watcher/public/models/execute_details/execute_details.js b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/execute_details/execute_details.js similarity index 100% rename from x-pack/legacy/plugins/watcher/public/models/execute_details/execute_details.js rename to x-pack/legacy/plugins/watcher/public/np_ready/application/models/execute_details/execute_details.js diff --git a/x-pack/legacy/plugins/watcher/public/models/execute_details/index.js b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/execute_details/index.js similarity index 100% rename from x-pack/legacy/plugins/watcher/public/models/execute_details/index.js rename to x-pack/legacy/plugins/watcher/public/np_ready/application/models/execute_details/index.js diff --git a/x-pack/legacy/plugins/watcher/public/np_ready/application/models/index.d.ts b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/index.d.ts new file mode 100644 index 0000000000000..a8ddb6ca2b76d --- /dev/null +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/index.d.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +declare module 'plugins/watcher/np_ready/application/models/visualize_options' { + export const VisualizeOptions: any; +} + +declare module 'plugins/watcher/np_ready/application/models/watch' { + export const Watch: any; +} + +declare module 'plugins/watcher/np_ready/application/models/watch/threshold_watch' { + export const ThresholdWatch: any; +} + +declare module 'plugins/watcher/np_ready/application/models/watch/json_watch' { + export const JsonWatch: any; +} + +declare module 'plugins/watcher/np_ready/application/models/execute_details/execute_details' { + export const ExecuteDetails: any; +} + +declare module 'plugins/watcher/np_ready/application/models/watch_history_item' { + export const WatchHistoryItem: any; +} + +declare module 'plugins/watcher/np_ready/application/models/watch_status' { + export const WatchStatus: any; +} + +declare module 'plugins/watcher/np_ready/application/models/settings' { + export const Settings: any; +} +declare module 'plugins/watcher/np_ready/application/models/action' { + export const Action: any; +} diff --git a/x-pack/legacy/plugins/watcher/public/models/settings/index.js b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/settings/index.js similarity index 100% rename from x-pack/legacy/plugins/watcher/public/models/settings/index.js rename to x-pack/legacy/plugins/watcher/public/np_ready/application/models/settings/index.js diff --git a/x-pack/legacy/plugins/watcher/public/models/settings/settings.js b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/settings/settings.js similarity index 100% rename from x-pack/legacy/plugins/watcher/public/models/settings/settings.js rename to x-pack/legacy/plugins/watcher/public/np_ready/application/models/settings/settings.js diff --git a/x-pack/legacy/plugins/watcher/public/models/visualize_options/index.js b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/visualize_options/index.js similarity index 100% rename from x-pack/legacy/plugins/watcher/public/models/visualize_options/index.js rename to x-pack/legacy/plugins/watcher/public/np_ready/application/models/visualize_options/index.js diff --git a/x-pack/legacy/plugins/watcher/public/models/visualize_options/visualize_options.js b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/visualize_options/visualize_options.js similarity index 100% rename from x-pack/legacy/plugins/watcher/public/models/visualize_options/visualize_options.js rename to x-pack/legacy/plugins/watcher/public/np_ready/application/models/visualize_options/visualize_options.js diff --git a/x-pack/legacy/plugins/watcher/public/models/watch/agg_types.ts b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch/agg_types.ts similarity index 94% rename from x-pack/legacy/plugins/watcher/public/models/watch/agg_types.ts rename to x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch/agg_types.ts index 65ab537889ea4..cefaaa3b1abd3 100644 --- a/x-pack/legacy/plugins/watcher/public/models/watch/agg_types.ts +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch/agg_types.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AGG_TYPES } from '../../../common/constants'; +import { AGG_TYPES } from '../../../../../common/constants'; export interface AggType { text: string; diff --git a/x-pack/legacy/plugins/watcher/public/models/watch/base_watch.js b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch/base_watch.js similarity index 100% rename from x-pack/legacy/plugins/watcher/public/models/watch/base_watch.js rename to x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch/base_watch.js diff --git a/x-pack/legacy/plugins/watcher/public/models/watch/comparators.ts b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch/comparators.ts similarity index 96% rename from x-pack/legacy/plugins/watcher/public/models/watch/comparators.ts rename to x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch/comparators.ts index b636cdaf14c18..edc3a03c25227 100644 --- a/x-pack/legacy/plugins/watcher/public/models/watch/comparators.ts +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch/comparators.ts @@ -6,7 +6,7 @@ import { i18n } from '@kbn/i18n'; -import { COMPARATORS } from '../../../common/constants'; +import { COMPARATORS } from '../../../../../common/constants'; export interface Comparator { text: string; diff --git a/x-pack/legacy/plugins/watcher/public/models/watch/default_watch.json b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch/default_watch.json similarity index 100% rename from x-pack/legacy/plugins/watcher/public/models/watch/default_watch.json rename to x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch/default_watch.json diff --git a/x-pack/legacy/plugins/watcher/public/models/watch/group_by_types.ts b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch/group_by_types.ts similarity index 100% rename from x-pack/legacy/plugins/watcher/public/models/watch/group_by_types.ts rename to x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch/group_by_types.ts diff --git a/x-pack/legacy/plugins/watcher/public/models/watch/index.js b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch/index.js similarity index 100% rename from x-pack/legacy/plugins/watcher/public/models/watch/index.js rename to x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch/index.js diff --git a/x-pack/legacy/plugins/watcher/public/models/watch/json_watch.js b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch/json_watch.js similarity index 98% rename from x-pack/legacy/plugins/watcher/public/models/watch/json_watch.js rename to x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch/json_watch.js index 3dd7af759970e..2e2ee47640cf0 100644 --- a/x-pack/legacy/plugins/watcher/public/models/watch/json_watch.js +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch/json_watch.js @@ -7,7 +7,7 @@ import uuid from 'uuid'; import { get } from 'lodash'; import { BaseWatch } from './base_watch'; -import { ACTION_TYPES, WATCH_TYPES } from '../../../common/constants'; +import { ACTION_TYPES, WATCH_TYPES } from '../../../../../common/constants'; import defaultWatchJson from './default_watch.json'; import { i18n } from '@kbn/i18n'; diff --git a/x-pack/legacy/plugins/watcher/public/models/watch/lib/check_action_id_collision/check_action_id_collision.js b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch/lib/check_action_id_collision/check_action_id_collision.js similarity index 100% rename from x-pack/legacy/plugins/watcher/public/models/watch/lib/check_action_id_collision/check_action_id_collision.js rename to x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch/lib/check_action_id_collision/check_action_id_collision.js diff --git a/x-pack/legacy/plugins/watcher/public/models/watch/lib/check_action_id_collision/index.js b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch/lib/check_action_id_collision/index.js similarity index 100% rename from x-pack/legacy/plugins/watcher/public/models/watch/lib/check_action_id_collision/index.js rename to x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch/lib/check_action_id_collision/index.js diff --git a/x-pack/legacy/plugins/watcher/public/models/watch/lib/create_action_id/create_action_id.js b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch/lib/create_action_id/create_action_id.js similarity index 100% rename from x-pack/legacy/plugins/watcher/public/models/watch/lib/create_action_id/create_action_id.js rename to x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch/lib/create_action_id/create_action_id.js diff --git a/x-pack/legacy/plugins/watcher/public/models/watch/lib/create_action_id/index.js b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch/lib/create_action_id/index.js similarity index 100% rename from x-pack/legacy/plugins/watcher/public/models/watch/lib/create_action_id/index.js rename to x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch/lib/create_action_id/index.js diff --git a/x-pack/legacy/plugins/watcher/public/models/watch/monitoring_watch.js b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch/monitoring_watch.js similarity index 92% rename from x-pack/legacy/plugins/watcher/public/models/watch/monitoring_watch.js rename to x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch/monitoring_watch.js index a0873934e1759..3269fcbe459d2 100644 --- a/x-pack/legacy/plugins/watcher/public/models/watch/monitoring_watch.js +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch/monitoring_watch.js @@ -5,7 +5,7 @@ */ import { BaseWatch } from './base_watch'; -import { WATCH_TYPES } from '../../../common/constants'; +import { WATCH_TYPES } from '../../../../../common/constants'; /** * {@code MonitoringWatch} system defined watches created by the Monitoring plugin. diff --git a/x-pack/legacy/plugins/watcher/public/models/watch/threshold_watch.js b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch/threshold_watch.js similarity index 99% rename from x-pack/legacy/plugins/watcher/public/models/watch/threshold_watch.js rename to x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch/threshold_watch.js index af995d6594a38..02fa99e7f3e16 100644 --- a/x-pack/legacy/plugins/watcher/public/models/watch/threshold_watch.js +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch/threshold_watch.js @@ -6,7 +6,7 @@ import { BaseWatch } from './base_watch'; import uuid from 'uuid'; -import { WATCH_TYPES, SORT_ORDERS, COMPARATORS } from '../../../common/constants'; +import { WATCH_TYPES, SORT_ORDERS, COMPARATORS } from '../../../../../common/constants'; import { getTimeUnitLabel } from '../../lib/get_time_unit_label'; import { i18n } from '@kbn/i18n'; import { aggTypes } from './agg_types'; diff --git a/x-pack/legacy/plugins/watcher/public/models/watch/watch.js b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch/watch.js similarity index 93% rename from x-pack/legacy/plugins/watcher/public/models/watch/watch.js rename to x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch/watch.js index d58a7799c6516..2723fed920675 100644 --- a/x-pack/legacy/plugins/watcher/public/models/watch/watch.js +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch/watch.js @@ -5,7 +5,7 @@ */ import { get, set } from 'lodash'; -import { WATCH_TYPES } from '../../../common/constants'; +import { WATCH_TYPES } from '../../../../../common/constants'; import { JsonWatch } from './json_watch'; import { ThresholdWatch } from './threshold_watch'; import { MonitoringWatch } from './monitoring_watch'; diff --git a/x-pack/legacy/plugins/watcher/public/models/watch_errors/index.js b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch_errors/index.js similarity index 100% rename from x-pack/legacy/plugins/watcher/public/models/watch_errors/index.js rename to x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch_errors/index.js diff --git a/x-pack/legacy/plugins/watcher/public/models/watch_errors/watch_errors.js b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch_errors/watch_errors.js similarity index 100% rename from x-pack/legacy/plugins/watcher/public/models/watch_errors/watch_errors.js rename to x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch_errors/watch_errors.js diff --git a/x-pack/legacy/plugins/watcher/public/models/watch_history_item/index.js b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch_history_item/index.js similarity index 100% rename from x-pack/legacy/plugins/watcher/public/models/watch_history_item/index.js rename to x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch_history_item/index.js diff --git a/x-pack/legacy/plugins/watcher/public/models/watch_history_item/watch_history_item.js b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch_history_item/watch_history_item.js similarity index 91% rename from x-pack/legacy/plugins/watcher/public/models/watch_history_item/watch_history_item.js rename to x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch_history_item/watch_history_item.js index a5918cec2764b..785f9d19b23dd 100644 --- a/x-pack/legacy/plugins/watcher/public/models/watch_history_item/watch_history_item.js +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch_history_item/watch_history_item.js @@ -6,7 +6,7 @@ import 'moment-duration-format'; import { get } from 'lodash'; -import { getMoment } from '../../../common/lib/get_moment'; +import { getMoment } from '../../../../../common/lib/get_moment'; import { WatchStatus } from '../watch_status'; export class WatchHistoryItem { diff --git a/x-pack/legacy/plugins/watcher/public/models/watch_status/index.js b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch_status/index.js similarity index 100% rename from x-pack/legacy/plugins/watcher/public/models/watch_status/index.js rename to x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch_status/index.js diff --git a/x-pack/legacy/plugins/watcher/public/models/watch_status/watch_status.js b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch_status/watch_status.js similarity index 94% rename from x-pack/legacy/plugins/watcher/public/models/watch_status/watch_status.js rename to x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch_status/watch_status.js index f213032a93c27..77007ea190386 100644 --- a/x-pack/legacy/plugins/watcher/public/models/watch_status/watch_status.js +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch_status/watch_status.js @@ -5,7 +5,7 @@ */ import { get } from 'lodash'; -import { getMoment } from '../../../common/lib/get_moment'; +import { getMoment } from '../../../../../common/lib/get_moment'; import { ActionStatus } from '../action_status'; export class WatchStatus { diff --git a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/json_watch_edit/index.ts b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/json_watch_edit/index.ts similarity index 100% rename from x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/json_watch_edit/index.ts rename to x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/json_watch_edit/index.ts diff --git a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/json_watch_edit/json_watch_edit.tsx b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/json_watch_edit/json_watch_edit.tsx similarity index 92% rename from x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/json_watch_edit/json_watch_edit.tsx rename to x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/json_watch_edit/json_watch_edit.tsx index 9c4b16e301b38..010e430c0719a 100644 --- a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/json_watch_edit/json_watch_edit.tsx +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/json_watch_edit/json_watch_edit.tsx @@ -16,10 +16,10 @@ import { EuiTitle, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { ExecuteDetails } from 'plugins/watcher/models/execute_details/execute_details'; -import { getActionType } from '../../../../../common/lib/get_action_type'; -import { BaseWatch, ExecutedWatchDetails } from '../../../../../common/types/watch_types'; -import { ACTION_MODES, TIME_UNITS } from '../../../../../common/constants'; +import { ExecuteDetails } from 'plugins/watcher/np_ready/application/models/execute_details/execute_details'; +import { getActionType } from '../../../../../../../common/lib/get_action_type'; +import { BaseWatch, ExecutedWatchDetails } from '../../../../../../../common/types/watch_types'; +import { ACTION_MODES, TIME_UNITS } from '../../../../../../../common/constants'; import { JsonWatchEditForm } from './json_watch_edit_form'; import { JsonWatchEditSimulate } from './json_watch_edit_simulate'; import { WatchContext } from '../../watch_context'; diff --git a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/json_watch_edit/json_watch_edit_form.tsx b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/json_watch_edit/json_watch_edit_form.tsx similarity index 94% rename from x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/json_watch_edit/json_watch_edit_form.tsx rename to x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/json_watch_edit/json_watch_edit_form.tsx index 02a54fc9b9279..376aeb205b855 100644 --- a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/json_watch_edit/json_watch_edit_form.tsx +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/json_watch_edit/json_watch_edit_form.tsx @@ -20,15 +20,20 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { serializeJsonWatch } from '../../../../../common/lib/serialization'; -import { ErrableFormRow, SectionError } from '../../../../components'; -import { putWatchApiUrl } from '../../../../lib/documentation_links'; +import { serializeJsonWatch } from '../../../../../../../common/lib/serialization'; +import { ErrableFormRow, SectionError, Error as ServerError } from '../../../../components'; import { onWatchSave } from '../../watch_edit_actions'; import { WatchContext } from '../../watch_context'; import { goToWatchList } from '../../../../lib/navigation'; import { RequestFlyout } from '../request_flyout'; +import { useAppContext } from '../../../../app_context'; export const JsonWatchEditForm = () => { + const { + links: { putWatchApiUrl }, + toasts, + } = useAppContext(); + const { watch, setWatchProperty } = useContext(WatchContext); const { errors } = watch.validate(); @@ -37,9 +42,7 @@ export const JsonWatchEditForm = () => { const [validationError, setValidationError] = useState(null); const [isRequestVisible, setIsRequestVisible] = useState(false); - const [serverError, setServerError] = useState<{ - data: { nessage: string; error: string }; - } | null>(null); + const [serverError, setServerError] = useState(null); const [isSaving, setIsSaving] = useState(false); @@ -192,7 +195,7 @@ export const JsonWatchEditForm = () => { isDisabled={hasErrors} onClick={async () => { setIsSaving(true); - const savedWatch = await onWatchSave(watch); + const savedWatch = await onWatchSave(watch, toasts); if (savedWatch && savedWatch.error) { const { data } = savedWatch.error; setIsSaving(false); diff --git a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/json_watch_edit/json_watch_edit_simulate.tsx b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/json_watch_edit/json_watch_edit_simulate.tsx similarity index 96% rename from x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/json_watch_edit/json_watch_edit_simulate.tsx rename to x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/json_watch_edit/json_watch_edit_simulate.tsx index e57a875aa4356..7c5de3d8e9298 100644 --- a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/json_watch_edit/json_watch_edit_simulate.tsx +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/json_watch_edit/json_watch_edit_simulate.tsx @@ -24,19 +24,19 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { ExecuteDetails } from 'plugins/watcher/models/execute_details/execute_details'; -import { WatchHistoryItem } from 'plugins/watcher/models/watch_history_item'; -import { ACTION_MODES, TIME_UNITS } from '../../../../../common/constants'; +import { ExecuteDetails } from 'plugins/watcher/np_ready/application/models/execute_details/execute_details'; +import { WatchHistoryItem } from 'plugins/watcher/np_ready/application/models/watch_history_item'; +import { ACTION_MODES, TIME_UNITS } from '../../../../../../../common/constants'; import { ExecutedWatchDetails, ExecutedWatchResults, -} from '../../../../../common/types/watch_types'; +} from '../../../../../../../common/types/watch_types'; import { ErrableFormRow } from '../../../../components/form_errors'; import { executeWatch } from '../../../../lib/api'; -import { executeWatchApiUrl } from '../../../../lib/documentation_links'; import { WatchContext } from '../../watch_context'; import { JsonWatchEditSimulateResults } from './json_watch_edit_simulate_results'; import { getTimeUnitLabel } from '../../../../lib/get_time_unit_label'; +import { useAppContext } from '../../../../app_context'; const actionModeOptions = Object.keys(ACTION_MODES).map(mode => ({ text: ACTION_MODES[mode], @@ -70,6 +70,9 @@ export const JsonWatchEditSimulate = ({ type: string; }>; }) => { + const { + links: { executeWatchApiUrl }, + } = useAppContext(); const { watch } = useContext(WatchContext); // hooks diff --git a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/json_watch_edit/json_watch_edit_simulate_results.tsx b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/json_watch_edit/json_watch_edit_simulate_results.tsx similarity index 99% rename from x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/json_watch_edit/json_watch_edit_simulate_results.tsx rename to x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/json_watch_edit/json_watch_edit_simulate_results.tsx index 1b2b4ab935e8c..4b630f5bc81b4 100644 --- a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/json_watch_edit/json_watch_edit_simulate_results.tsx +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/json_watch_edit/json_watch_edit_simulate_results.tsx @@ -21,7 +21,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { ExecutedWatchDetails, ExecutedWatchResults, -} from '../../../../../common/types/watch_types'; +} from '../../../../../../../common/types/watch_types'; import { getTypeFromAction } from '../../watch_edit_actions'; import { WatchContext } from '../../watch_context'; import { WatchStatus, SectionError } from '../../../../components'; diff --git a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/monitoring_watch_edit/index.ts b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/monitoring_watch_edit/index.ts similarity index 100% rename from x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/monitoring_watch_edit/index.ts rename to x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/monitoring_watch_edit/index.ts diff --git a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/monitoring_watch_edit/monitoring_watch_edit.tsx b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/monitoring_watch_edit/monitoring_watch_edit.tsx similarity index 100% rename from x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/monitoring_watch_edit/monitoring_watch_edit.tsx rename to x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/monitoring_watch_edit/monitoring_watch_edit.tsx diff --git a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/request_flyout.tsx b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/request_flyout.tsx similarity index 100% rename from x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/request_flyout.tsx rename to x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/request_flyout.tsx diff --git a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/action_fields/email_action_fields.tsx b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/action_fields/email_action_fields.tsx similarity index 97% rename from x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/action_fields/email_action_fields.tsx rename to x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/action_fields/email_action_fields.tsx index aebe8baaee417..3e70e49f42350 100644 --- a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/action_fields/email_action_fields.tsx +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/action_fields/email_action_fields.tsx @@ -8,7 +8,7 @@ import React, { Fragment } from 'react'; import { EuiComboBox, EuiFieldText, EuiFormRow, EuiTextArea } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { ErrableFormRow } from '../../../../../components/form_errors'; -import { EmailAction } from '../../../../../../common/types/action_types'; +import { EmailAction } from '../../../../../../../../common/types/action_types'; interface Props { action: EmailAction; diff --git a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/action_fields/index.ts b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/action_fields/index.ts similarity index 100% rename from x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/action_fields/index.ts rename to x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/action_fields/index.ts diff --git a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/action_fields/index_action_fields.tsx b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/action_fields/index_action_fields.tsx similarity index 94% rename from x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/action_fields/index_action_fields.tsx rename to x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/action_fields/index_action_fields.tsx index 1cafb08ca4060..b7ab76d9890bc 100644 --- a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/action_fields/index_action_fields.tsx +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/action_fields/index_action_fields.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { EuiFieldText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { ErrableFormRow } from '../../../../../components/form_errors'; -import { IndexAction } from '../../../../../../common/types/action_types'; +import { IndexAction } from '../../../../../../../../common/types/action_types'; interface Props { action: IndexAction; diff --git a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/action_fields/jira_action_fields.tsx b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/action_fields/jira_action_fields.tsx similarity index 97% rename from x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/action_fields/jira_action_fields.tsx rename to x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/action_fields/jira_action_fields.tsx index b8bdeaff90821..c09b3c44fde65 100644 --- a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/action_fields/jira_action_fields.tsx +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/action_fields/jira_action_fields.tsx @@ -8,7 +8,7 @@ import React, { Fragment } from 'react'; import { EuiFieldText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { ErrableFormRow } from '../../../../../components/form_errors'; -import { JiraAction } from '../../../../../../common/types/action_types'; +import { JiraAction } from '../../../../../../../../common/types/action_types'; interface Props { action: JiraAction; diff --git a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/action_fields/logging_action_fields.tsx b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/action_fields/logging_action_fields.tsx similarity index 94% rename from x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/action_fields/logging_action_fields.tsx rename to x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/action_fields/logging_action_fields.tsx index b70e504519ae5..7da2a22ecd6c4 100644 --- a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/action_fields/logging_action_fields.tsx +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/action_fields/logging_action_fields.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { EuiFieldText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { ErrableFormRow } from '../../../../../components/form_errors'; -import { LoggingAction } from '../../../../../../common/types/action_types'; +import { LoggingAction } from '../../../../../../../../common/types/action_types'; interface Props { action: LoggingAction; diff --git a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/action_fields/pagerduty_action_fields.tsx b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/action_fields/pagerduty_action_fields.tsx similarity index 95% rename from x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/action_fields/pagerduty_action_fields.tsx rename to x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/action_fields/pagerduty_action_fields.tsx index b2b670bf6b91f..3287bdefa08aa 100644 --- a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/action_fields/pagerduty_action_fields.tsx +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/action_fields/pagerduty_action_fields.tsx @@ -7,7 +7,7 @@ import React, { Fragment } from 'react'; import { EuiFieldText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { ErrableFormRow } from '../../../../../components/form_errors'; -import { PagerDutyAction } from '../../../../../../common/types/action_types'; +import { PagerDutyAction } from '../../../../../../../../common/types/action_types'; interface Props { action: PagerDutyAction; diff --git a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/action_fields/slack_action_fields.tsx b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/action_fields/slack_action_fields.tsx similarity index 96% rename from x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/action_fields/slack_action_fields.tsx rename to x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/action_fields/slack_action_fields.tsx index 7b5a598c97eb7..a72cf232d8d09 100644 --- a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/action_fields/slack_action_fields.tsx +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/action_fields/slack_action_fields.tsx @@ -6,7 +6,7 @@ import React, { Fragment } from 'react'; import { EuiComboBox, EuiTextArea, EuiFormRow } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { SlackAction } from '../../../../../../common/types/action_types'; +import { SlackAction } from '../../../../../../../../common/types/action_types'; interface Props { action: SlackAction; diff --git a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/action_fields/webhook_action_fields.tsx b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/action_fields/webhook_action_fields.tsx similarity index 98% rename from x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/action_fields/webhook_action_fields.tsx rename to x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/action_fields/webhook_action_fields.tsx index c3784e1ca5516..bdc6f0bcbb717 100644 --- a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/action_fields/webhook_action_fields.tsx +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/action_fields/webhook_action_fields.tsx @@ -18,7 +18,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { ErrableFormRow } from '../../../../../components/form_errors'; -import { WebhookAction } from '../../../../../../common/types/action_types'; +import { WebhookAction } from '../../../../../../../../common/types/action_types'; interface Props { action: WebhookAction; @@ -39,7 +39,7 @@ export const WebhookActionFields: React.FunctionComponent = ({ useEffect(() => { editAction({ key: 'contentType', value: 'application/json' }); // set content-type for threshold watch to json by default - }, []); + }, [editAction]); return ( diff --git a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/index.ts b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/index.ts similarity index 100% rename from x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/index.ts rename to x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/index.ts diff --git a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/threshold_watch_action_accordion.tsx b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/threshold_watch_action_accordion.tsx similarity index 91% rename from x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/threshold_watch_action_accordion.tsx rename to x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/threshold_watch_action_accordion.tsx index 8b72eb7f19456..4fca772a18217 100644 --- a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/threshold_watch_action_accordion.tsx +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/threshold_watch_action_accordion.tsx @@ -21,13 +21,12 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { ExecuteDetails } from 'plugins/watcher/models/execute_details/execute_details'; -import { Action } from 'plugins/watcher/models/action'; -import { toastNotifications } from 'ui/notify'; -import { WatchHistoryItem } from 'plugins/watcher/models/watch_history_item'; -import { ThresholdWatch } from 'plugins/watcher/models/watch/threshold_watch'; -import { ActionType } from '../../../../../common/types/action_types'; -import { ACTION_TYPES, ACTION_MODES } from '../../../../../common/constants'; +import { ExecuteDetails } from 'plugins/watcher/np_ready/application/models/execute_details/execute_details'; +import { Action } from 'plugins/watcher/np_ready/application/models/action'; +import { WatchHistoryItem } from 'plugins/watcher/np_ready/application/models/watch_history_item'; +import { ThresholdWatch } from 'plugins/watcher/np_ready/application/models/watch/threshold_watch'; +import { ActionType } from '../../../../../../../common/types/action_types'; +import { ACTION_TYPES, ACTION_MODES } from '../../../../../../../common/constants'; import { WatchContext } from '../../watch_context'; import { WebhookActionFields, @@ -39,8 +38,8 @@ import { JiraActionFields, } from './action_fields'; import { executeWatch } from '../../../../lib/api'; -import { watchActionsConfigurationMap } from '../../../../lib/documentation_links'; import { SectionError } from '../../../../components'; +import { useAppContext } from '../../../../app_context'; const actionFieldsComponentMap = { [ACTION_TYPES.LOGGING]: LoggingActionFields, @@ -71,6 +70,10 @@ export const WatchActionsAccordion: React.FunctionComponent = ({ settings, actionErrors, }) => { + const { + links: { watchActionsConfigurationMap }, + toasts, + } = useAppContext(); const { watch, setWatchProperty } = useContext(WatchContext); const { actions } = watch; @@ -238,9 +241,9 @@ export const WatchActionsAccordion: React.FunctionComponent = ({ if (actionStatus && actionStatus.lastExecutionSuccessful === false) { const message = actionStatus.lastExecutionReason || action.simulateFailMessage; - return toastNotifications.addDanger(message); + return toasts.addDanger(message); } - return toastNotifications.addSuccess(action.simulateMessage); + return toasts.addSuccess(action.simulateMessage); }} > {action.simulatePrompt} diff --git a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/threshold_watch_action_dropdown.tsx b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/threshold_watch_action_dropdown.tsx similarity index 96% rename from x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/threshold_watch_action_dropdown.tsx rename to x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/threshold_watch_action_dropdown.tsx index 82f3352b4e023..d92cccfa00f14 100644 --- a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/threshold_watch_action_dropdown.tsx +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/threshold_watch_action_dropdown.tsx @@ -16,9 +16,9 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useContext, useState } from 'react'; -import { Action } from 'plugins/watcher/models/action'; +import { Action } from 'plugins/watcher/np_ready/application/models/action'; import { FormattedMessage } from '@kbn/i18n/react'; -import { ACTION_TYPES } from '../../../../../common/constants'; +import { ACTION_TYPES } from '../../../../../../../common/constants'; import { WatchContext } from '../../watch_context'; const disabledMessage = i18n.translate( diff --git a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/threshold_watch_action_panel.tsx b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/threshold_watch_action_panel.tsx similarity index 93% rename from x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/threshold_watch_action_panel.tsx rename to x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/threshold_watch_action_panel.tsx index a2e46652429ea..6072f93e53cf6 100644 --- a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/threshold_watch_action_panel.tsx +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/threshold_watch_action_panel.tsx @@ -6,7 +6,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiTitle, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useContext } from 'react'; -import { loadSettings } from '../../../../lib/api'; +import { useLoadSettings } from '../../../../lib/api'; import { WatchActionsDropdown } from './threshold_watch_action_dropdown'; import { WatchActionsAccordion } from './threshold_watch_action_accordion'; import { WatchContext } from '../../watch_context'; @@ -22,7 +22,7 @@ interface Props { export const WatchActionsPanel: React.FunctionComponent = ({ actionErrors }) => { const { watch } = useContext(WatchContext); - const { data: settings, isLoading } = loadSettings(); + const { data: settings, isLoading } = useLoadSettings(); return (
diff --git a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/threshold_watch_edit.tsx b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/threshold_watch_edit.tsx similarity index 95% rename from x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/threshold_watch_edit.tsx rename to x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/threshold_watch_edit.tsx index 910d4f1e0b15c..f1b5d2c9eab7b 100644 --- a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/threshold_watch_edit.tsx +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/threshold_watch_edit.tsx @@ -26,9 +26,9 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { TIME_UNITS } from '../../../../../common/constants'; -import { serializeThresholdWatch } from '../../../../../common/lib/serialization'; -import { ErrableFormRow, SectionError } from '../../../../components'; +import { TIME_UNITS } from '../../../../../../../common/constants'; +import { serializeThresholdWatch } from '../../../../../../../common/lib/serialization'; +import { ErrableFormRow, SectionError, Error as ServerError } from '../../../../components'; import { fetchFields, getMatchingIndices, loadIndexPatterns } from '../../../../lib/api'; import { aggTypes } from '../../../../models/watch/agg_types'; import { groupByTypes } from '../../../../models/watch/group_by_types'; @@ -40,6 +40,7 @@ import { WatchActionsPanel } from './threshold_watch_action_panel'; import { getTimeUnitLabel } from '../../../../lib/get_time_unit_label'; import { goToWatchList } from '../../../../lib/navigation'; import { RequestFlyout } from '../request_flyout'; +import { useAppContext } from '../../../../app_context'; const expressionFieldsWithValidation = [ 'aggField', @@ -104,7 +105,7 @@ const getTimeFieldOptions = (fields: any) => { }; interface IOption { label: string; - options: Array<{ value: string; label: string }>; + options: Array<{ value: string; label: string; key?: string }>; } const getIndexOptions = async (patternString: string, indexPatterns: string[]) => { @@ -129,12 +130,14 @@ const getIndexOptions = async (patternString: string, indexPatterns: string[]) = defaultMessage: 'Based on your indices and index patterns', } ), - options: matchingOptions.map(match => { - return { - label: match, - value: match, - }; - }), + options: matchingOptions + .map(match => { + return { + label: match, + value: match, + }; + }) + .sort((a, b) => String(a.label).localeCompare(b.label)), }); } @@ -144,6 +147,7 @@ const getIndexOptions = async (patternString: string, indexPatterns: string[]) = }), options: [ { + key: 'UNIQUE_CHOOSE_KEY', value: patternString, label: patternString, }, @@ -155,7 +159,8 @@ const getIndexOptions = async (patternString: string, indexPatterns: string[]) = export const ThresholdWatchEdit = ({ pageTitle }: { pageTitle: string }) => { // hooks - const [indexPatterns, setIndexPatterns] = useState([]); + const { toasts } = useAppContext(); + const [indexPatterns, setIndexPatterns] = useState([]); const [esFields, setEsFields] = useState([]); const [indexOptions, setIndexOptions] = useState([]); const [timeFieldOptions, setTimeFieldOptions] = useState([firstFieldOption]); @@ -165,34 +170,33 @@ export const ThresholdWatchEdit = ({ pageTitle }: { pageTitle: string }) => { const [watchThresholdPopoverOpen, setWatchThresholdPopoverOpen] = useState(false); const [watchDurationPopoverOpen, setWatchDurationPopoverOpen] = useState(false); const [aggTypePopoverOpen, setAggTypePopoverOpen] = useState(false); - const [serverError, setServerError] = useState<{ - data: { nessage: string; error: string }; - } | null>(null); + const [serverError, setServerError] = useState(null); const [isSaving, setIsSaving] = useState(false); const [isIndiciesLoading, setIsIndiciesLoading] = useState(false); const [isRequestVisible, setIsRequestVisible] = useState(false); const { watch, setWatchProperty } = useContext(WatchContext); - const getIndexPatterns = async () => { - const indexPatternObjects = await loadIndexPatterns(); - const titles = indexPatternObjects.map((indexPattern: any) => indexPattern.attributes.title); - setIndexPatterns(titles); - }; + useEffect(() => { + const getIndexPatterns = async () => { + const indexPatternObjects = await loadIndexPatterns(); + const titles = indexPatternObjects.map((indexPattern: any) => indexPattern.attributes.title); + setIndexPatterns(titles); + }; - const loadData = async () => { - if (watch.index && watch.index.length > 0) { - const allEsFields = await getFields(watch.index); - const timeFields = getTimeFieldOptions(allEsFields); - setEsFields(allEsFields); - setTimeFieldOptions(timeFields); - setWatchProperty('timeFields', timeFields); - } - getIndexPatterns(); - }; + const loadData = async () => { + if (watch.index && watch.index.length > 0) { + const allEsFields = await getFields(watch.index); + const timeFields = getTimeFieldOptions(allEsFields); + setEsFields(allEsFields); + setTimeFieldOptions(timeFields); + setWatchProperty('timeFields', timeFields); + } + getIndexPatterns(); + }; - useEffect(() => { loadData(); + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const { errors } = watch.validate(); @@ -899,7 +903,7 @@ export const ThresholdWatchEdit = ({ pageTitle }: { pageTitle: string }) => { isLoading={isSaving} onClick={async () => { setIsSaving(true); - const savedWatch = await onWatchSave(watch); + const savedWatch = await onWatchSave(watch, toasts); if (savedWatch && savedWatch.error) { setIsSaving(false); return setServerError(savedWatch.error); diff --git a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/watch_visualization.tsx b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/watch_visualization.tsx similarity index 83% rename from x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/watch_visualization.tsx rename to x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/watch_visualization.tsx index 772f3cc024fe8..a3da7d14c8886 100644 --- a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/watch_visualization.tsx +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/watch_visualization.tsx @@ -18,20 +18,20 @@ import { ScaleType, Settings, } from '@elastic/charts'; -import { TimeBuckets } from 'ui/time_buckets'; import dateMath from '@elastic/datemath'; -import chrome from 'ui/chrome'; import moment from 'moment-timezone'; +import { IUiSettingsClient } from 'src/core/public'; import { EuiCallOut, EuiLoadingChart, EuiSpacer, EuiEmptyPrompt, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { VisualizeOptions } from 'plugins/watcher/models/visualize_options'; -import { ThresholdWatch } from 'plugins/watcher/models/watch/threshold_watch'; -import { npStart } from 'ui/new_platform'; -import { getWatchVisualizationData } from '../../../../lib/api'; +import { VisualizeOptions } from 'plugins/watcher/np_ready/application/models/visualize_options'; +import { ThresholdWatch } from 'plugins/watcher/np_ready/application/models/watch/threshold_watch'; + +import { useGetWatchVisualizationData } from '../../../../lib/api'; import { WatchContext } from '../../watch_context'; import { aggTypes } from '../../../../models/watch/agg_types'; import { comparators } from '../../../../models/watch/comparators'; import { SectionError, Error } from '../../../../components'; +import { useAppContext } from '../../../../app_context'; const customTheme = () => { return { @@ -46,8 +46,7 @@ const customTheme = () => { }; }; -const getTimezone = () => { - const config = chrome.getUiSettingsClient(); +const getTimezone = (config: IUiSettingsClient) => { const DATE_FORMAT_CONFIG_KEY = 'dateFormat:tz'; const isCustomTimezone = !config.isDefault(DATE_FORMAT_CONFIG_KEY); if (isCustomTimezone) { @@ -59,8 +58,7 @@ const getTimezone = () => { return detectedTimezone; } // default to UTC if we can't figure out the timezone - const tzOffset = moment().format('Z'); - return tzOffset; + return moment().format('Z'); }; const getDomain = (watch: any) => { @@ -83,16 +81,20 @@ const getThreshold = (watch: any) => { return watch.threshold.slice(0, comparators[watch.thresholdComparator].requiredValues); }; -const getTimeBuckets = (watch: any) => { +const getTimeBuckets = (watch: any, timeBuckets: any) => { const domain = getDomain(watch); - const timeBuckets = new TimeBuckets(); timeBuckets.setBounds(domain); return timeBuckets; }; export const WatchVisualization = () => { + const { + legacy: { TimeBuckets }, + euiUtils, + uiSettings, + } = useAppContext(); const { watch } = useContext(WatchContext); - const chartsTheme = npStart.plugins.eui_utils.useChartsTheme(); + const chartsTheme = euiUtils.useChartsTheme(); const { index, timeField, @@ -117,7 +119,7 @@ export const WatchVisualization = () => { rangeFrom: domain.min, rangeTo: domain.max, interval, - timezone: getTimezone(), + timezone: getTimezone(uiSettings), }); // Fetching visualization data is independent of watch actions @@ -129,30 +131,33 @@ export const WatchVisualization = () => { data: watchVisualizationData, error, sendRequest: reload, - } = getWatchVisualizationData(watchWithoutActions, visualizeOptions); + } = useGetWatchVisualizationData(watchWithoutActions, visualizeOptions); - useEffect(() => { - // Prevent sending a second request on initial render. - if (isInitialRequest) { - return; - } - - reload(); - }, [ - index, - timeField, - triggerIntervalSize, - triggerIntervalUnit, - aggType, - aggField, - termSize, - termField, - thresholdComparator, - timeWindowSize, - timeWindowUnit, - groupBy, - threshold, - ]); + useEffect( + () => { + // Prevent sending a second request on initial render. + if (isInitialRequest) { + return; + } + reload(); + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [ + index, + timeField, + triggerIntervalSize, + triggerIntervalUnit, + aggType, + aggField, + termSize, + termField, + thresholdComparator, + timeWindowSize, + timeWindowUnit, + groupBy, + threshold, + ] + ); if (isInitialRequest && isLoading) { return ( @@ -190,7 +195,7 @@ export const WatchVisualization = () => { if (watchVisualizationData) { const watchVisualizationDataKeys = Object.keys(watchVisualizationData); - const timezone = getTimezone(); + const timezone = getTimezone(uiSettings); const actualThreshold = getThreshold(watch); let maxY = actualThreshold[actualThreshold.length - 1]; @@ -204,7 +209,7 @@ export const WatchVisualization = () => { const dateFormatter = (d: number) => { return moment(d) .tz(timezone) - .format(getTimeBuckets(watch).getScaledDateFormat()); + .format(getTimeBuckets(watch, new TimeBuckets()).getScaledDateFormat()); }; const aggLabel = aggTypes[watch.aggType].text; return ( diff --git a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/watch_edit.tsx b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/watch_edit.tsx similarity index 82% rename from x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/watch_edit.tsx rename to x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/watch_edit.tsx index 25daf190dc1b1..9f252d3e542e0 100644 --- a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/watch_edit.tsx +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/watch_edit.tsx @@ -9,13 +9,11 @@ import { isEqual } from 'lodash'; import { EuiPageContent } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import chrome from 'ui/chrome'; -import { MANAGEMENT_BREADCRUMB } from 'ui/management'; -import { Watch } from 'plugins/watcher/models/watch'; +import { Watch } from 'plugins/watcher/np_ready/application/models/watch'; import { FormattedMessage } from '@kbn/i18n/react'; -import { WATCH_TYPES } from '../../../../common/constants'; -import { BaseWatch } from '../../../../common/types/watch_types'; +import { WATCH_TYPES } from '../../../../../../common/constants'; +import { BaseWatch } from '../../../../../../common/types/watch_types'; import { getPageErrorCode, PageError, SectionLoading, SectionError } from '../../../components'; import { loadWatch } from '../../../lib/api'; import { listBreadcrumb, editBreadcrumb, createBreadcrumb } from '../../../lib/breadcrumbs'; @@ -23,6 +21,7 @@ import { JsonWatchEdit } from './json_watch_edit'; import { ThresholdWatchEdit } from './threshold_watch_edit'; import { MonitoringWatchEdit } from './monitoring_watch_edit'; import { WatchContext } from '../watch_context'; +import { useAppContext } from '../../../app_context'; const getTitle = (watch: BaseWatch) => { if (watch.isNew) { @@ -97,6 +96,10 @@ export const WatchEdit = ({ }; }) => { // hooks + const { + legacy: { MANAGEMENT_BREADCRUMB }, + chrome, + } = useAppContext(); const [{ watch, loadError }, dispatch] = useReducer(watchReducer, { watch: null }); const setWatchProperty = (property: string, value: any) => { @@ -107,33 +110,33 @@ export const WatchEdit = ({ dispatch({ command: 'addAction', payload: action }); }; - const getWatch = async () => { - if (id) { - try { - const loadedWatch = await loadWatch(id); - dispatch({ command: 'setWatch', payload: loadedWatch }); - } catch (error) { - dispatch({ command: 'setError', payload: error }); - } - } else if (type) { - const WatchType = Watch.getWatchTypes()[type]; - if (WatchType) { - dispatch({ command: 'setWatch', payload: new WatchType() }); + useEffect(() => { + const getWatch = async () => { + if (id) { + try { + const loadedWatch = await loadWatch(id); + dispatch({ command: 'setWatch', payload: loadedWatch }); + } catch (error) { + dispatch({ command: 'setError', payload: error }); + } + } else if (type) { + const WatchType = Watch.getWatchTypes()[type]; + if (WatchType) { + dispatch({ command: 'setWatch', payload: new WatchType() }); + } } - } - }; + }; - useEffect(() => { getWatch(); - }, []); + }, [id, type]); useEffect(() => { - chrome.breadcrumbs.set([ + chrome.setBreadcrumbs([ MANAGEMENT_BREADCRUMB, listBreadcrumb, id ? editBreadcrumb : createBreadcrumb, ]); - }, [id]); + }, [id, chrome, MANAGEMENT_BREADCRUMB]); const errorCode = getPageErrorCode(loadError); if (errorCode) { diff --git a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/watch_context.ts b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/watch_context.ts similarity index 100% rename from x-pack/legacy/plugins/watcher/public/sections/watch_edit/watch_context.ts rename to x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/watch_context.ts diff --git a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/watch_edit_actions.ts b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/watch_edit_actions.ts similarity index 86% rename from x-pack/legacy/plugins/watcher/public/sections/watch_edit/watch_edit_actions.ts rename to x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/watch_edit_actions.ts index 320ba59e0589e..b93c2c510047d 100644 --- a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/watch_edit_actions.ts +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/watch_edit_actions.ts @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ToastsSetup } from 'src/core/public'; import { i18n } from '@kbn/i18n'; -import { toastNotifications } from 'ui/notify'; import { get } from 'lodash'; -import { ACTION_TYPES, WATCH_TYPES } from '../../../common/constants'; -import { BaseWatch } from '../../../common/types/watch_types'; +import { ACTION_TYPES, WATCH_TYPES } from '../../../../../common/constants'; +import { BaseWatch } from '../../../../../common/types/watch_types'; import { createWatch } from '../../lib/api'; import { goToWatchList } from '../../lib/navigation'; @@ -62,10 +62,10 @@ function createActionsForWatch(watchInstance: BaseWatch) { return watchInstance; } -export async function saveWatch(watch: BaseWatch): Promise { +export async function saveWatch(watch: BaseWatch, toasts: ToastsSetup): Promise { try { await createWatch(watch); - toastNotifications.addSuccess( + toasts.addSuccess( i18n.translate('xpack.watcher.sections.watchEdit.json.saveSuccessNotificationText', { defaultMessage: "Saved '{watchDisplayName}'", values: { @@ -75,11 +75,11 @@ export async function saveWatch(watch: BaseWatch): Promise { ); goToWatchList(); } catch (error) { - return error.response ? { error: error.response } : { error }; + return { error: error?.response.data ?? (error.body || error) }; } } -export async function onWatchSave(watch: BaseWatch): Promise { +export async function onWatchSave(watch: BaseWatch, toasts: ToastsSetup): Promise { const watchActions = watch.watch && watch.watch.actions; const watchData = watchActions ? createActionsForWatch(watch) : watch; @@ -109,7 +109,7 @@ export async function onWatchSave(watch: BaseWatch): Promise { }, }; } - return saveWatch(watchData); + return saveWatch(watchData, toasts); } - return saveWatch(watchData); + return saveWatch(watchData, toasts); } diff --git a/x-pack/legacy/plugins/watcher/public/sections/watch_list/components/watch_list.tsx b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_list/components/watch_list.tsx similarity index 97% rename from x-pack/legacy/plugins/watcher/public/sections/watch_list/components/watch_list.tsx rename to x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_list/components/watch_list.tsx index d5191c56643c2..b2afc0b92509b 100644 --- a/x-pack/legacy/plugins/watcher/public/sections/watch_list/components/watch_list.tsx +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_list/components/watch_list.tsx @@ -27,10 +27,8 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { Moment } from 'moment'; -import chrome from 'ui/chrome'; -import { MANAGEMENT_BREADCRUMB } from 'ui/management'; -import { REFRESH_INTERVALS, PAGINATION, WATCH_TYPES } from '../../../../common/constants'; +import { REFRESH_INTERVALS, PAGINATION, WATCH_TYPES } from '../../../../../../common/constants'; import { listBreadcrumb } from '../../../lib/breadcrumbs'; import { getPageErrorCode, @@ -41,12 +39,17 @@ import { SectionLoading, Error, } from '../../../components'; -import { loadWatches } from '../../../lib/api'; -import { watcherGettingStartedUrl } from '../../../lib/documentation_links'; +import { useLoadWatches } from '../../../lib/api'; import { goToCreateThresholdAlert, goToCreateAdvancedWatch } from '../../../lib/navigation'; +import { useAppContext } from '../../../app_context'; export const WatchList = () => { // hooks + const { + chrome, + legacy: { MANAGEMENT_BREADCRUMB }, + links: { watcherGettingStartedUrl }, + } = useAppContext(); const [selection, setSelection] = useState([]); const [watchesToDelete, setWatchesToDelete] = useState([]); // Filter out deleted watches on the client, because the API will return 200 even though some watches @@ -54,10 +57,10 @@ export const WatchList = () => { const [deletedWatches, setDeletedWatches] = useState([]); useEffect(() => { - chrome.breadcrumbs.set([MANAGEMENT_BREADCRUMB, listBreadcrumb]); - }, []); + chrome.setBreadcrumbs([MANAGEMENT_BREADCRUMB, listBreadcrumb]); + }, [chrome, MANAGEMENT_BREADCRUMB]); - const { isLoading: isWatchesLoading, data: watches, error } = loadWatches( + const { isLoading: isWatchesLoading, data: watches, error } = useLoadWatches( REFRESH_INTERVALS.WATCH_LIST ); diff --git a/x-pack/legacy/plugins/watcher/public/sections/watch_status/components/watch_detail.tsx b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_status/components/watch_detail.tsx similarity index 96% rename from x-pack/legacy/plugins/watcher/public/sections/watch_status/components/watch_detail.tsx rename to x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_status/components/watch_detail.tsx index aba4fd0c52a2e..197342bba4180 100644 --- a/x-pack/legacy/plugins/watcher/public/sections/watch_status/components/watch_detail.tsx +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_status/components/watch_detail.tsx @@ -7,7 +7,6 @@ import React, { Fragment, useState, useEffect, useContext } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { toastNotifications } from 'ui/notify'; import { EuiInMemoryTable, @@ -21,8 +20,9 @@ import { } from '@elastic/eui'; import { ackWatchAction } from '../../../lib/api'; import { WatchStatus } from '../../../components'; -import { PAGINATION } from '../../../../common/constants'; +import { PAGINATION } from '../../../../../../common/constants'; import { WatchDetailsContext } from '../watch_details_context'; +import { useAppContext } from '../../../app_context'; interface ActionError { code: string; @@ -36,6 +36,7 @@ interface ActionStatus { } export const WatchDetail = () => { + const { toasts } = useAppContext(); const { watchDetail } = useContext(WatchDetailsContext); const [actionStatuses, setActionStatuses] = useState([]); @@ -60,7 +61,7 @@ export const WatchDetail = () => { }; }); setActionStatuses(actionStatusesWithErrors); - }, [watchDetail]); + }, [watchDetail, actionErrors, currentActionStatuses]); const baseColumns = [ { @@ -144,7 +145,7 @@ export const WatchDetail = () => { return setActionStatuses(newActionStatusesWithErrors); } catch (e) { setIsActionStatusLoading(false); - toastNotifications.addDanger( + toasts.addDanger( i18n.translate( 'xpack.watcher.sections.watchDetail.watchTable.ackActionErrorMessage', { diff --git a/x-pack/legacy/plugins/watcher/public/sections/watch_status/components/watch_history.tsx b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_status/components/watch_history.tsx similarity index 97% rename from x-pack/legacy/plugins/watcher/public/sections/watch_status/components/watch_history.tsx rename to x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_status/components/watch_history.tsx index bf6ca0c6c43a0..2bc1a0cbace18 100644 --- a/x-pack/legacy/plugins/watcher/public/sections/watch_status/components/watch_history.tsx +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_status/components/watch_history.tsx @@ -23,9 +23,9 @@ import { EuiTitle, } from '@elastic/eui'; -import { PAGINATION } from '../../../../common/constants'; +import { PAGINATION } from '../../../../../../common/constants'; import { WatchStatus, SectionError, Error } from '../../../components'; -import { loadWatchHistory, loadWatchHistoryDetail } from '../../../lib/api'; +import { useLoadWatchHistory, useLoadWatchHistoryDetail } from '../../../lib/api'; import { WatchDetailsContext } from '../watch_details_context'; const watchHistoryTimeSpanOptions = [ @@ -83,12 +83,12 @@ export const WatchHistory = () => { setIsActivated(isActive); } - const { error: historyError, data: history, isLoading } = loadWatchHistory( + const { error: historyError, data: history, isLoading } = useLoadWatchHistory( loadedWatch.id, watchHistoryTimeSpan ); - const { error: watchHistoryDetailsError, data: watchHistoryDetails } = loadWatchHistoryDetail( + const { error: watchHistoryDetailsError, data: watchHistoryDetails } = useLoadWatchHistoryDetail( detailWatchId ); diff --git a/x-pack/legacy/plugins/watcher/public/sections/watch_status/components/watch_status.tsx b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_status/components/watch_status.tsx similarity index 94% rename from x-pack/legacy/plugins/watcher/public/sections/watch_status/components/watch_status.tsx rename to x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_status/components/watch_status.tsx index 413e8f638887b..53817c23e72eb 100644 --- a/x-pack/legacy/plugins/watcher/public/sections/watch_status/components/watch_status.tsx +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_status/components/watch_status.tsx @@ -17,15 +17,12 @@ import { EuiBadge, EuiButtonEmpty, } from '@elastic/eui'; -import chrome from 'ui/chrome'; -import { MANAGEMENT_BREADCRUMB } from 'ui/management'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { toastNotifications } from 'ui/notify'; import { WatchDetail } from './watch_detail'; import { WatchHistory } from './watch_history'; import { listBreadcrumb, statusBreadcrumb } from '../../../lib/breadcrumbs'; -import { loadWatchDetail, deactivateWatch, activateWatch } from '../../../lib/api'; +import { useLoadWatchDetail, deactivateWatch, activateWatch } from '../../../lib/api'; import { WatchDetailsContext } from '../watch_details_context'; import { getPageErrorCode, @@ -34,6 +31,7 @@ import { DeleteWatchesModal, } from '../../../components'; import { goToWatchList } from '../../../lib/navigation'; +import { useAppContext } from '../../../app_context'; interface WatchStatusTab { id: string; @@ -69,11 +67,16 @@ export const WatchStatus = ({ }; }; }) => { + const { + chrome, + legacy: { MANAGEMENT_BREADCRUMB }, + toasts, + } = useAppContext(); const { error: watchDetailError, data: watchDetail, isLoading: isWatchDetailLoading, - } = loadWatchDetail(id); + } = useLoadWatchDetail(id); const [selectedTab, setSelectedTab] = useState(WATCH_EXECUTION_HISTORY_TAB); const [isActivated, setIsActivated] = useState(undefined); @@ -81,8 +84,8 @@ export const WatchStatus = ({ const [isTogglingActivation, setIsTogglingActivation] = useState(false); useEffect(() => { - chrome.breadcrumbs.set([MANAGEMENT_BREADCRUMB, listBreadcrumb, statusBreadcrumb]); - }, [id]); + chrome.setBreadcrumbs([MANAGEMENT_BREADCRUMB, listBreadcrumb, statusBreadcrumb]); + }, [id, chrome, MANAGEMENT_BREADCRUMB]); const errorCode = getPageErrorCode(watchDetailError); @@ -148,7 +151,7 @@ export const WatchStatus = ({ defaultMessage: "Couldn't activate watch", } ); - return toastNotifications.addDanger(message); + return toasts.addDanger(message); } setIsActivated(!isActivated); diff --git a/x-pack/legacy/plugins/watcher/public/sections/watch_status/watch_details_context.ts b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_status/watch_details_context.ts similarity index 100% rename from x-pack/legacy/plugins/watcher/public/sections/watch_status/watch_details_context.ts rename to x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_status/watch_details_context.ts diff --git a/x-pack/legacy/plugins/watcher/public/shared_imports.ts b/x-pack/legacy/plugins/watcher/public/np_ready/application/shared_imports.ts similarity index 79% rename from x-pack/legacy/plugins/watcher/public/shared_imports.ts rename to x-pack/legacy/plugins/watcher/public/np_ready/application/shared_imports.ts index 3d93b882733ab..60445b00c0985 100644 --- a/x-pack/legacy/plugins/watcher/public/shared_imports.ts +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/shared_imports.ts @@ -10,4 +10,4 @@ export { UseRequestConfig, sendRequest, useRequest, -} from '../../../../../src/plugins/es_ui_shared/public/request'; +} from '../../../../../../../src/plugins/es_ui_shared/public/request/np_ready_request'; diff --git a/x-pack/legacy/plugins/watcher/public/index.js b/x-pack/legacy/plugins/watcher/public/np_ready/index.ts similarity index 71% rename from x-pack/legacy/plugins/watcher/public/index.js rename to x-pack/legacy/plugins/watcher/public/np_ready/index.ts index c1b84e76d0008..ff635579316e5 100644 --- a/x-pack/legacy/plugins/watcher/public/index.js +++ b/x-pack/legacy/plugins/watcher/public/np_ready/index.ts @@ -3,6 +3,6 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { WatcherUIPlugin } from './plugin'; -import './register_route'; -import './register_management_sections'; +export const plugin = () => new WatcherUIPlugin(); diff --git a/x-pack/legacy/plugins/watcher/public/np_ready/plugin.ts b/x-pack/legacy/plugins/watcher/public/np_ready/plugin.ts new file mode 100644 index 0000000000000..161de9b5fc060 --- /dev/null +++ b/x-pack/legacy/plugins/watcher/public/np_ready/plugin.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Plugin, CoreSetup, CoreStart } from 'src/core/public'; + +import { LegacyDependencies } from './types'; + +interface LegacyPlugins { + __LEGACY: LegacyDependencies; +} + +export class WatcherUIPlugin implements Plugin { + /* TODO: Remove this in future. We need this at mount (setup) but it's only available on start plugins. */ + euiUtils: any = null; + + setup({ application, notifications, http, uiSettings }: CoreSetup, { __LEGACY }: LegacyPlugins) { + application.register({ + id: 'watcher', + title: 'Watcher', + mount: async ( + { + core: { + docLinks, + chrome, + // Waiting for types to be updated. + // @ts-ignore + savedObjects, + i18n: { Context: I18nContext }, + }, + }, + { element } + ) => { + const euiUtils = this.euiUtils!; + const { boot } = await import('./application/boot'); + return boot({ + element, + toasts: notifications.toasts, + http, + uiSettings, + docLinks, + chrome, + euiUtils, + savedObjects: savedObjects.client, + I18nContext, + legacy: { + ...__LEGACY, + }, + }); + }, + }); + } + + start(core: CoreStart, { eui_utils }: any) { + // eslint-disable-next-line @typescript-eslint/camelcase + this.euiUtils = eui_utils; + } + + stop() {} +} diff --git a/x-pack/legacy/plugins/watcher/server/routes/api/fields/register_fields_routes.js b/x-pack/legacy/plugins/watcher/public/np_ready/types.ts similarity index 63% rename from x-pack/legacy/plugins/watcher/server/routes/api/fields/register_fields_routes.js rename to x-pack/legacy/plugins/watcher/public/np_ready/types.ts index 64b9a14f9c438..22109f99c2c48 100644 --- a/x-pack/legacy/plugins/watcher/server/routes/api/fields/register_fields_routes.js +++ b/x-pack/legacy/plugins/watcher/public/np_ready/types.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { registerListRoute } from './register_list_route'; - -export function registerFieldsRoutes(server) { - registerListRoute(server); +export interface LegacyDependencies { + MANAGEMENT_BREADCRUMB: { text: string; href?: string }; + TimeBuckets: any; + licenseStatus: any; } diff --git a/x-pack/legacy/plugins/watcher/public/register_feature.js b/x-pack/legacy/plugins/watcher/public/register_feature.js deleted file mode 100644 index 5dd4f28f03bc5..0000000000000 --- a/x-pack/legacy/plugins/watcher/public/register_feature.js +++ /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; - * you may not use this file except in compliance with the Elastic License. - */ - - - -import { FeatureCatalogueRegistryProvider, FeatureCatalogueCategory } from 'ui/registry/feature_catalogue'; -import { i18n } from '@kbn/i18n'; - -FeatureCatalogueRegistryProvider.register(() => { - return { - id: 'watcher', - title: 'Watcher', // This is a product name so we don't translate it. - description: i18n.translate('xpack.watcher.watcherDescription', { - defaultMessage: 'Detect changes in your data by creating, managing, and monitoring alerts.' - }), - icon: 'watchesApp', - path: '/app/kibana#/management/elasticsearch/watcher/watches', - showOnHomePage: true, - category: FeatureCatalogueCategory.ADMIN - }; -}); diff --git a/x-pack/legacy/plugins/watcher/public/register_feature.ts b/x-pack/legacy/plugins/watcher/public/register_feature.ts new file mode 100644 index 0000000000000..0de41e09f788e --- /dev/null +++ b/x-pack/legacy/plugins/watcher/public/register_feature.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { npSetup } from 'ui/new_platform'; +import { FeatureCatalogueCategory } from 'ui/registry/feature_catalogue'; + +npSetup.plugins.home.featureCatalogue.register({ + id: 'watcher', + title: 'Watcher', // This is a product name so we don't translate it. + category: FeatureCatalogueCategory.ADMIN, + description: i18n.translate('xpack.watcher.watcherDescription', { + defaultMessage: 'Detect changes in your data by creating, managing, and monitoring alerts.', + }), + icon: 'watchesApp', + path: '/app/kibana#/management/elasticsearch/watcher/watches', + showOnHomePage: true, +}); diff --git a/x-pack/legacy/plugins/watcher/public/register_management_sections.js b/x-pack/legacy/plugins/watcher/public/register_management_sections.js deleted file mode 100644 index 886ac7d28db64..0000000000000 --- a/x-pack/legacy/plugins/watcher/public/register_management_sections.js +++ /dev/null @@ -1,60 +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; - * you may not use this file except in compliance with the Elastic License. - */ - - -import { management } from 'ui/management'; -import { i18n } from '@kbn/i18n'; - -management.getSection('elasticsearch').register('watcher', { - display: i18n.translate('xpack.watcher.sections.watchList.managementSection.watcherDisplayName', { - defaultMessage: 'Watcher', - }), - order: 6, - url: '#/management/elasticsearch/watcher/', -}); - -management.getSection('elasticsearch/watcher').register('watches', { - display: i18n.translate('xpack.watcher.sections.watchList.managementSection.watchesDisplayName', { - defaultMessage: 'Watches', - }), - order: 1, -}); - -management.getSection('elasticsearch/watcher').register('watch', { - visible: false, -}); - -management.getSection('elasticsearch/watcher/watch').register('status', { - display: i18n.translate('xpack.watcher.sections.watchList.managementSection.statusDisplayName', { - defaultMessage: 'Status', - }), - order: 1, - visible: false, -}); - -management.getSection('elasticsearch/watcher/watch').register('edit', { - display: i18n.translate('xpack.watcher.sections.watchList.managementSection.editDisplayName', { - defaultMessage: 'Edit', - }), - order: 2, - visible: false, -}); - -management.getSection('elasticsearch/watcher/watch').register('new', { - display: i18n.translate( - 'xpack.watcher.sections.watchList.managementSection.newWatchDisplayName', - { - defaultMessage: 'New Watch', - } - ), - order: 1, - visible: false, -}); - -management.getSection('elasticsearch/watcher/watch').register('history-item', { - order: 1, - visible: false, -}); diff --git a/x-pack/legacy/plugins/watcher/public/register_route.js b/x-pack/legacy/plugins/watcher/public/register_route.js deleted file mode 100644 index c58be17bc6e75..0000000000000 --- a/x-pack/legacy/plugins/watcher/public/register_route.js +++ /dev/null @@ -1,68 +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; - * you may not use this file except in compliance with the Elastic License. - */ -import React from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; -import { SavedObjectsClientProvider } from 'ui/saved_objects'; -import routes from 'ui/routes'; -import { management } from 'ui/management'; -import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; -import template from './app.html'; -import { App } from './app'; -import { setHttpClient, setSavedObjectsClient } from './lib/api'; -import { I18nContext } from 'ui/i18n'; -import { manageAngularLifecycle } from './lib/manage_angular_lifecycle'; -import { PLUGIN } from '../common/constants'; -import { LICENSE_STATUS_UNAVAILABLE, LICENSE_STATUS_INVALID } from '../../../common/constants'; - -let elem; -const renderReact = async (elem, licenseStatus) => { - render( - - - , - elem - ); -}; -routes.when('/management/elasticsearch/watcher/:param1?/:param2?/:param3?/:param4?', { - template, - controller: class WatcherController { - constructor($injector, $scope, $http, Private) { - const $route = $injector.get('$route'); - const licenseStatus = xpackInfo.get(`features.${PLUGIN.ID}`); - - // clean up previously rendered React app if one exists - // this happens because of React Router redirects - elem && unmountComponentAtNode(elem); - setSavedObjectsClient(Private(SavedObjectsClientProvider)); - // NOTE: We depend upon Angular's $http service because it's decorated with interceptors, - // e.g. to check license status per request. - setHttpClient($http); - $scope.$$postDigest(() => { - elem = document.getElementById('watchReactRoot'); - renderReact(elem, licenseStatus); - manageAngularLifecycle($scope, $route, elem); - }); - } - }, - controllerAs: 'watchRoute', -}); - -routes.defaults(/\/management/, { - resolve: { - watcherManagementSection: () => { - const watchesSection = management.getSection('elasticsearch/watcher'); - const licenseStatus = xpackInfo.get(`features.${PLUGIN.ID}`); - const { status } = licenseStatus; - - if (status === LICENSE_STATUS_INVALID || status === LICENSE_STATUS_UNAVAILABLE) { - return watchesSection.hide(); - } - - watchesSection.show(); - - }, - }, -}); diff --git a/x-pack/legacy/plugins/watcher/server/lib/call_with_internal_user_factory/call_with_internal_user_factory.js b/x-pack/legacy/plugins/watcher/server/lib/call_with_internal_user_factory/call_with_internal_user_factory.js deleted file mode 100644 index b0ca090601062..0000000000000 --- a/x-pack/legacy/plugins/watcher/server/lib/call_with_internal_user_factory/call_with_internal_user_factory.js +++ /dev/null @@ -1,18 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { once } from 'lodash'; - -const _callWithInternalUser = once((server) => { - const { callWithInternalUser } = server.plugins.elasticsearch.getCluster('admin'); - return callWithInternalUser; -}); - -export const callWithInternalUserFactory = (server) => { - return (...args) => { - return _callWithInternalUser(server)(...args); - }; -}; diff --git a/x-pack/legacy/plugins/watcher/server/lib/call_with_request_factory/call_with_request_factory.js b/x-pack/legacy/plugins/watcher/server/lib/call_with_request_factory/call_with_request_factory.js deleted file mode 100644 index f60f825b98004..0000000000000 --- a/x-pack/legacy/plugins/watcher/server/lib/call_with_request_factory/call_with_request_factory.js +++ /dev/null @@ -1,21 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { once } from 'lodash'; -import { elasticsearchJsPlugin } from '../elasticsearch_js_plugin'; - -const callWithRequest = once((server) => { - const config = { plugins: [ elasticsearchJsPlugin ] }; - const cluster = server.plugins.elasticsearch.createCluster('watcher', config); - - return cluster.callWithRequest; -}); - -export const callWithRequestFactory = (server, request) => { - return (...args) => { - return callWithRequest(server)(request, ...args); - }; -}; diff --git a/x-pack/legacy/plugins/watcher/server/lib/call_with_request_factory/index.js b/x-pack/legacy/plugins/watcher/server/lib/call_with_request_factory/index.js deleted file mode 100644 index 787814d87dff9..0000000000000 --- a/x-pack/legacy/plugins/watcher/server/lib/call_with_request_factory/index.js +++ /dev/null @@ -1,7 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -export { callWithRequestFactory } from './call_with_request_factory'; diff --git a/x-pack/legacy/plugins/watcher/server/lib/elasticsearch_js_plugin/index.js b/x-pack/legacy/plugins/watcher/server/lib/elasticsearch_js_plugin/index.js deleted file mode 100644 index 87b5ff5426c9d..0000000000000 --- a/x-pack/legacy/plugins/watcher/server/lib/elasticsearch_js_plugin/index.js +++ /dev/null @@ -1,7 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -export { elasticsearchJsPlugin } from './elasticsearch_js_plugin'; diff --git a/x-pack/legacy/plugins/watcher/server/lib/error_wrappers/__tests__/wrap_custom_error.js b/x-pack/legacy/plugins/watcher/server/lib/error_wrappers/__tests__/wrap_custom_error.js deleted file mode 100644 index f9c102be7a1ff..0000000000000 --- a/x-pack/legacy/plugins/watcher/server/lib/error_wrappers/__tests__/wrap_custom_error.js +++ /dev/null @@ -1,21 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from '@kbn/expect'; -import { wrapCustomError } from '../wrap_custom_error'; - -describe('wrap_custom_error', () => { - describe('#wrapCustomError', () => { - it('should return a Boom object', () => { - const originalError = new Error('I am an error'); - const statusCode = 404; - const wrappedError = wrapCustomError(originalError, statusCode); - - expect(wrappedError.isBoom).to.be(true); - expect(wrappedError.output.statusCode).to.equal(statusCode); - }); - }); -}); diff --git a/x-pack/legacy/plugins/watcher/server/lib/error_wrappers/__tests__/wrap_es_error.js b/x-pack/legacy/plugins/watcher/server/lib/error_wrappers/__tests__/wrap_es_error.js deleted file mode 100644 index 467cc4fcdae1f..0000000000000 --- a/x-pack/legacy/plugins/watcher/server/lib/error_wrappers/__tests__/wrap_es_error.js +++ /dev/null @@ -1,39 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from '@kbn/expect'; -import { wrapEsError } from '../wrap_es_error'; - -describe('wrap_es_error', () => { - describe('#wrapEsError', () => { - - let originalError; - beforeEach(() => { - originalError = new Error('I am an error'); - originalError.statusCode = 404; - }); - - it('should return a Boom object', () => { - const wrappedError = wrapEsError(originalError); - - expect(wrappedError.isBoom).to.be(true); - }); - - it('should return the correct Boom object', () => { - const wrappedError = wrapEsError(originalError); - - expect(wrappedError.output.statusCode).to.be(originalError.statusCode); - expect(wrappedError.output.payload.message).to.be(originalError.message); - }); - - it('should return the correct Boom object with custom message', () => { - const wrappedError = wrapEsError(originalError, { 404: 'No encontrado!' }); - - expect(wrappedError.output.statusCode).to.be(originalError.statusCode); - expect(wrappedError.output.payload.message).to.be('No encontrado!'); - }); - }); -}); diff --git a/x-pack/legacy/plugins/watcher/server/lib/error_wrappers/__tests__/wrap_unknown_error.js b/x-pack/legacy/plugins/watcher/server/lib/error_wrappers/__tests__/wrap_unknown_error.js deleted file mode 100644 index 85e0b2b3033ad..0000000000000 --- a/x-pack/legacy/plugins/watcher/server/lib/error_wrappers/__tests__/wrap_unknown_error.js +++ /dev/null @@ -1,19 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from '@kbn/expect'; -import { wrapUnknownError } from '../wrap_unknown_error'; - -describe('wrap_unknown_error', () => { - describe('#wrapUnknownError', () => { - it('should return a Boom object', () => { - const originalError = new Error('I am an error'); - const wrappedError = wrapUnknownError(originalError); - - expect(wrappedError.isBoom).to.be(true); - }); - }); -}); diff --git a/x-pack/legacy/plugins/watcher/server/lib/error_wrappers/index.js b/x-pack/legacy/plugins/watcher/server/lib/error_wrappers/index.js deleted file mode 100644 index f275f15637091..0000000000000 --- a/x-pack/legacy/plugins/watcher/server/lib/error_wrappers/index.js +++ /dev/null @@ -1,9 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -export { wrapCustomError } from './wrap_custom_error'; -export { wrapEsError } from './wrap_es_error'; -export { wrapUnknownError } from './wrap_unknown_error'; diff --git a/x-pack/legacy/plugins/watcher/server/lib/error_wrappers/wrap_custom_error.js b/x-pack/legacy/plugins/watcher/server/lib/error_wrappers/wrap_custom_error.js deleted file mode 100644 index 3295113d38ee5..0000000000000 --- a/x-pack/legacy/plugins/watcher/server/lib/error_wrappers/wrap_custom_error.js +++ /dev/null @@ -1,18 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import Boom from 'boom'; - -/** - * Wraps a custom error into a Boom error response and returns it - * - * @param err Object error - * @param statusCode Error status code - * @return Object Boom error response - */ -export function wrapCustomError(err, statusCode) { - return Boom.boomify(err, { statusCode }); -} diff --git a/x-pack/legacy/plugins/watcher/server/lib/error_wrappers/wrap_es_error.js b/x-pack/legacy/plugins/watcher/server/lib/error_wrappers/wrap_es_error.js deleted file mode 100644 index 2df2e4b802e1a..0000000000000 --- a/x-pack/legacy/plugins/watcher/server/lib/error_wrappers/wrap_es_error.js +++ /dev/null @@ -1,30 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import Boom from 'boom'; - -/** - * Wraps an error thrown by the ES JS client into a Boom error response and returns it - * - * @param err Object Error thrown by ES JS client - * @param statusCodeToMessageMap Object Optional map of HTTP status codes => error messages - * @return Object Boom error response - */ -export function wrapEsError(err, statusCodeToMessageMap = {}) { - - const statusCode = err.statusCode; - - // If no custom message if specified for the error's status code, just - // wrap the error as a Boom error response and return it - if (!statusCodeToMessageMap[statusCode]) { - return Boom.boomify(err, { statusCode }); - } - - // Otherwise, use the custom message to create a Boom error response and - // return it - const message = statusCodeToMessageMap[statusCode]; - return new Boom(message, { statusCode }); -} diff --git a/x-pack/legacy/plugins/watcher/server/lib/error_wrappers/wrap_unknown_error.js b/x-pack/legacy/plugins/watcher/server/lib/error_wrappers/wrap_unknown_error.js deleted file mode 100644 index ffd915c513362..0000000000000 --- a/x-pack/legacy/plugins/watcher/server/lib/error_wrappers/wrap_unknown_error.js +++ /dev/null @@ -1,17 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import Boom from 'boom'; - -/** - * Wraps an unknown error into a Boom error response and returns it - * - * @param err Object Unknown error - * @return Object Boom error response - */ -export function wrapUnknownError(err) { - return Boom.boomify(err); -} diff --git a/x-pack/legacy/plugins/watcher/server/lib/is_es_error_factory/__tests__/is_es_error_factory.js b/x-pack/legacy/plugins/watcher/server/lib/is_es_error_factory/__tests__/is_es_error_factory.js deleted file mode 100644 index 76fdf7b36c3d0..0000000000000 --- a/x-pack/legacy/plugins/watcher/server/lib/is_es_error_factory/__tests__/is_es_error_factory.js +++ /dev/null @@ -1,48 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from '@kbn/expect'; -import { isEsErrorFactory } from '../is_es_error_factory'; -import { set } from 'lodash'; - -class MockAbstractEsError {} - -describe('is_es_error_factory', () => { - - let mockServer; - let isEsError; - - beforeEach(() => { - const mockEsErrors = { - _Abstract: MockAbstractEsError - }; - mockServer = {}; - set(mockServer, 'plugins.elasticsearch.getCluster', () => ({ errors: mockEsErrors })); - - isEsError = isEsErrorFactory(mockServer); - }); - - describe('#isEsErrorFactory', () => { - - it('should return a function', () => { - expect(isEsError).to.be.a(Function); - }); - - describe('returned function', () => { - - it('should return true if passed-in err is a known esError', () => { - const knownEsError = new MockAbstractEsError(); - expect(isEsError(knownEsError)).to.be(true); - }); - - it('should return false if passed-in err is not a known esError', () => { - const unknownEsError = {}; - expect(isEsError(unknownEsError)).to.be(false); - - }); - }); - }); -}); diff --git a/x-pack/legacy/plugins/watcher/server/lib/is_es_error_factory/index.js b/x-pack/legacy/plugins/watcher/server/lib/is_es_error_factory/index.js deleted file mode 100644 index 441648a8701e0..0000000000000 --- a/x-pack/legacy/plugins/watcher/server/lib/is_es_error_factory/index.js +++ /dev/null @@ -1,7 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -export { isEsErrorFactory } from './is_es_error_factory'; diff --git a/x-pack/legacy/plugins/watcher/server/lib/is_es_error_factory/is_es_error_factory.js b/x-pack/legacy/plugins/watcher/server/lib/is_es_error_factory/is_es_error_factory.js deleted file mode 100644 index 80daac5bd496d..0000000000000 --- a/x-pack/legacy/plugins/watcher/server/lib/is_es_error_factory/is_es_error_factory.js +++ /dev/null @@ -1,18 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { memoize } from 'lodash'; - -const esErrorsFactory = memoize((server) => { - return server.plugins.elasticsearch.getCluster('admin').errors; -}); - -export function isEsErrorFactory(server) { - const esErrors = esErrorsFactory(server); - return function isEsError(err) { - return err instanceof esErrors._Abstract; - }; -} diff --git a/x-pack/legacy/plugins/watcher/server/lib/license_pre_routing_factory/license_pre_routing_factory.js b/x-pack/legacy/plugins/watcher/server/lib/license_pre_routing_factory/license_pre_routing_factory.js deleted file mode 100644 index 5b34108c9c1c0..0000000000000 --- a/x-pack/legacy/plugins/watcher/server/lib/license_pre_routing_factory/license_pre_routing_factory.js +++ /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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { once } from 'lodash'; -import { wrapCustomError } from '../error_wrappers'; -import { PLUGIN } from '../../../common/constants'; -import { LICENSE_STATUS_VALID } from '../../../../../common/constants/license_status'; - -export const licensePreRoutingFactory = once((server) => { - const xpackMainPlugin = server.plugins.xpack_main; - - // License checking and enable/disable logic - function licensePreRouting() { - const licenseCheckResults = xpackMainPlugin.info.feature(PLUGIN.ID).getLicenseCheckResults(); - const { status } = licenseCheckResults; - - if (status !== LICENSE_STATUS_VALID) { - const error = new Error(licenseCheckResults.message); - const statusCode = 403; - throw wrapCustomError(error, statusCode); - } - - return null; - } - - return licensePreRouting; -}); - diff --git a/x-pack/legacy/plugins/watcher/server/lib/call_with_internal_user_factory/index.js b/x-pack/legacy/plugins/watcher/server/np_ready/index.ts similarity index 55% rename from x-pack/legacy/plugins/watcher/server/lib/call_with_internal_user_factory/index.js rename to x-pack/legacy/plugins/watcher/server/np_ready/index.ts index a56a50e2864a5..3f5e1a91209ea 100644 --- a/x-pack/legacy/plugins/watcher/server/lib/call_with_internal_user_factory/index.js +++ b/x-pack/legacy/plugins/watcher/server/np_ready/index.ts @@ -3,5 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { PluginInitializerContext } from 'src/core/server'; +import { WatcherServerPlugin } from './plugin'; -export { callWithInternalUserFactory } from './call_with_internal_user_factory'; +export const plugin = (ctx: PluginInitializerContext) => new WatcherServerPlugin(); diff --git a/x-pack/legacy/plugins/watcher/server/np_ready/lib/call_with_request_factory.ts b/x-pack/legacy/plugins/watcher/server/np_ready/lib/call_with_request_factory.ts new file mode 100644 index 0000000000000..eaec9cd91b23c --- /dev/null +++ b/x-pack/legacy/plugins/watcher/server/np_ready/lib/call_with_request_factory.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ElasticsearchServiceSetup } from 'src/core/server'; +import { once } from 'lodash'; +import { elasticsearchJsPlugin } from './elasticsearch_js_plugin'; + +const callWithRequest = once((elasticsearchService: ElasticsearchServiceSetup) => { + const config = { plugins: [elasticsearchJsPlugin] }; + return elasticsearchService.createClient('watcher', config); +}); + +export const callWithRequestFactory = ( + elasticsearchService: ElasticsearchServiceSetup, + request: any +) => { + return (...args: any[]) => { + return ( + callWithRequest(elasticsearchService) + .asScoped(request) + // @ts-ignore + .callAsCurrentUser(...args) + ); + }; +}; diff --git a/x-pack/legacy/plugins/watcher/server/lib/elasticsearch_js_plugin/elasticsearch_js_plugin.js b/x-pack/legacy/plugins/watcher/server/np_ready/lib/elasticsearch_js_plugin.ts similarity index 84% rename from x-pack/legacy/plugins/watcher/server/lib/elasticsearch_js_plugin/elasticsearch_js_plugin.js rename to x-pack/legacy/plugins/watcher/server/np_ready/lib/elasticsearch_js_plugin.ts index ad42388beea1e..240e93e160fe0 100644 --- a/x-pack/legacy/plugins/watcher/server/lib/elasticsearch_js_plugin/elasticsearch_js_plugin.js +++ b/x-pack/legacy/plugins/watcher/server/np_ready/lib/elasticsearch_js_plugin.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -export const elasticsearchJsPlugin = (Client, config, components) => { +export const elasticsearchJsPlugin = (Client: any, config: any, components: any) => { const ca = components.clientAction.factory; Client.prototype.watcher = components.clientAction.namespaceFactory(); @@ -21,19 +21,19 @@ export const elasticsearchJsPlugin = (Client, config, components) => { params: { masterTimeout: { name: 'master_timeout', - type: 'duration' - } + type: 'duration', + }, }, url: { fmt: '/_watcher/watch/<%=id%>/_deactivate', req: { id: { type: 'string', - required: true - } - } + required: true, + }, + }, }, - method: 'PUT' + method: 'PUT', }); /** @@ -47,19 +47,19 @@ export const elasticsearchJsPlugin = (Client, config, components) => { params: { masterTimeout: { name: 'master_timeout', - type: 'duration' - } + type: 'duration', + }, }, url: { fmt: '/_watcher/watch/<%=id%>/_activate', req: { id: { type: 'string', - required: true - } - } + required: true, + }, + }, }, - method: 'PUT' + method: 'PUT', }); /** @@ -74,23 +74,23 @@ export const elasticsearchJsPlugin = (Client, config, components) => { params: { masterTimeout: { name: 'master_timeout', - type: 'duration' - } + type: 'duration', + }, }, url: { fmt: '/_watcher/watch/<%=id%>/_ack/<%=action%>', req: { id: { type: 'string', - required: true + required: true, }, action: { type: 'string', - required: true - } - } + required: true, + }, + }, }, - method: 'POST' + method: 'POST', }); /** @@ -105,22 +105,22 @@ export const elasticsearchJsPlugin = (Client, config, components) => { params: { masterTimeout: { name: 'master_timeout', - type: 'duration' + type: 'duration', }, force: { - type: 'boolean' - } + type: 'boolean', + }, }, url: { fmt: '/_watcher/watch/<%=id%>', req: { id: { type: 'string', - required: true - } - } + required: true, + }, + }, }, - method: 'DELETE' + method: 'DELETE', }); /** @@ -132,14 +132,14 @@ export const elasticsearchJsPlugin = (Client, config, components) => { params: { masterTimeout: { name: 'master_timeout', - type: 'duration' - } + type: 'duration', + }, }, url: { - fmt: '/_watcher/watch/_execute' + fmt: '/_watcher/watch/_execute', }, needBody: true, - method: 'POST' + method: 'POST', }); /** @@ -155,10 +155,10 @@ export const elasticsearchJsPlugin = (Client, config, components) => { req: { id: { type: 'string', - required: true - } - } - } + required: true, + }, + }, + }, }); /** @@ -172,20 +172,20 @@ export const elasticsearchJsPlugin = (Client, config, components) => { params: { masterTimeout: { name: 'master_timeout', - type: 'duration' - } + type: 'duration', + }, }, url: { fmt: '/_watcher/watch/<%=id%>', req: { id: { type: 'string', - required: true - } - } + required: true, + }, + }, }, needBody: true, - method: 'PUT' + method: 'PUT', }); /** @@ -196,9 +196,9 @@ export const elasticsearchJsPlugin = (Client, config, components) => { watcher.restart = ca({ params: {}, url: { - fmt: '/_watcher/_restart' + fmt: '/_watcher/_restart', }, - method: 'PUT' + method: 'PUT', }); /** @@ -209,9 +209,9 @@ export const elasticsearchJsPlugin = (Client, config, components) => { watcher.start = ca({ params: {}, url: { - fmt: '/_watcher/_start' + fmt: '/_watcher/_start', }, - method: 'PUT' + method: 'PUT', }); /** @@ -222,8 +222,8 @@ export const elasticsearchJsPlugin = (Client, config, components) => { watcher.stats = ca({ params: {}, url: { - fmt: '/_watcher/stats' - } + fmt: '/_watcher/stats', + }, }); /** @@ -234,8 +234,8 @@ export const elasticsearchJsPlugin = (Client, config, components) => { watcher.stop = ca({ params: {}, url: { - fmt: '/_watcher/_stop' + fmt: '/_watcher/_stop', }, - method: 'PUT' + method: 'PUT', }); }; diff --git a/x-pack/legacy/plugins/watcher/server/lib/fetch_all_from_scroll/__tests__/fetch_all_from_scroll.js b/x-pack/legacy/plugins/watcher/server/np_ready/lib/fetch_all_from_scroll/__tests__/fetch_all_from_scroll.js similarity index 100% rename from x-pack/legacy/plugins/watcher/server/lib/fetch_all_from_scroll/__tests__/fetch_all_from_scroll.js rename to x-pack/legacy/plugins/watcher/server/np_ready/lib/fetch_all_from_scroll/__tests__/fetch_all_from_scroll.js diff --git a/x-pack/legacy/plugins/watcher/server/lib/fetch_all_from_scroll/fetch_all_from_scroll.js b/x-pack/legacy/plugins/watcher/server/np_ready/lib/fetch_all_from_scroll/fetch_all_from_scroll.ts similarity index 64% rename from x-pack/legacy/plugins/watcher/server/lib/fetch_all_from_scroll/fetch_all_from_scroll.js rename to x-pack/legacy/plugins/watcher/server/np_ready/lib/fetch_all_from_scroll/fetch_all_from_scroll.ts index eb76d5d3731cf..d762b05f01d79 100644 --- a/x-pack/legacy/plugins/watcher/server/lib/fetch_all_from_scroll/fetch_all_from_scroll.js +++ b/x-pack/legacy/plugins/watcher/server/np_ready/lib/fetch_all_from_scroll/fetch_all_from_scroll.ts @@ -5,9 +5,9 @@ */ import { get } from 'lodash'; -import { ES_SCROLL_SETTINGS } from '../../../common/constants'; +import { ES_SCROLL_SETTINGS } from '../../../../common/constants'; -export function fetchAllFromScroll(response, callWithRequest, hits = []) { +export function fetchAllFromScroll(response: any, callWithRequest: any, hits: any[] = []) { const newHits = get(response, 'hits.hits', []); const scrollId = get(response, '_scroll_id'); @@ -17,12 +17,11 @@ export function fetchAllFromScroll(response, callWithRequest, hits = []) { return callWithRequest('scroll', { body: { scroll: ES_SCROLL_SETTINGS.KEEPALIVE, - scroll_id: scrollId - } - }) - .then(innerResponse => { - return fetchAllFromScroll(innerResponse, callWithRequest, hits); - }); + scroll_id: scrollId, + }, + }).then((innerResponse: any) => { + return fetchAllFromScroll(innerResponse, callWithRequest, hits); + }); } return Promise.resolve(hits); diff --git a/x-pack/legacy/plugins/watcher/server/lib/fetch_all_from_scroll/index.js b/x-pack/legacy/plugins/watcher/server/np_ready/lib/fetch_all_from_scroll/index.ts similarity index 100% rename from x-pack/legacy/plugins/watcher/server/lib/fetch_all_from_scroll/index.js rename to x-pack/legacy/plugins/watcher/server/np_ready/lib/fetch_all_from_scroll/index.ts diff --git a/x-pack/legacy/plugins/watcher/public/lib/documentation_links/index.ts b/x-pack/legacy/plugins/watcher/server/np_ready/lib/is_es_error/index.ts similarity index 84% rename from x-pack/legacy/plugins/watcher/public/lib/documentation_links/index.ts rename to x-pack/legacy/plugins/watcher/server/np_ready/lib/is_es_error/index.ts index 81e0c494e28b3..a9a3c61472d8c 100644 --- a/x-pack/legacy/plugins/watcher/public/lib/documentation_links/index.ts +++ b/x-pack/legacy/plugins/watcher/server/np_ready/lib/is_es_error/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './documentation_links'; +export { isEsError } from './is_es_error'; diff --git a/x-pack/legacy/plugins/watcher/server/routes/api/history/register_history_routes.js b/x-pack/legacy/plugins/watcher/server/np_ready/lib/is_es_error/is_es_error.ts similarity index 55% rename from x-pack/legacy/plugins/watcher/server/routes/api/history/register_history_routes.js rename to x-pack/legacy/plugins/watcher/server/np_ready/lib/is_es_error/is_es_error.ts index bef26fbb9b267..4137293cf39c0 100644 --- a/x-pack/legacy/plugins/watcher/server/routes/api/history/register_history_routes.js +++ b/x-pack/legacy/plugins/watcher/server/np_ready/lib/is_es_error/is_es_error.ts @@ -4,8 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { registerLoadRoute } from './register_load_route'; +import * as legacyElasticsearch from 'elasticsearch'; -export function registerHistoryRoutes(server) { - registerLoadRoute(server); +const esErrorsParent = legacyElasticsearch.errors._Abstract; + +export function isEsError(err: Error) { + return err instanceof esErrorsParent; } diff --git a/x-pack/legacy/plugins/watcher/server/lib/license_pre_routing_factory/__tests__/license_pre_routing_factory.js b/x-pack/legacy/plugins/watcher/server/np_ready/lib/license_pre_routing_factory/__tests__/license_pre_routing_factory.js similarity index 71% rename from x-pack/legacy/plugins/watcher/server/lib/license_pre_routing_factory/__tests__/license_pre_routing_factory.js rename to x-pack/legacy/plugins/watcher/server/np_ready/lib/license_pre_routing_factory/__tests__/license_pre_routing_factory.js index ed4a51a11b7cd..fc01e42e6fdf2 100644 --- a/x-pack/legacy/plugins/watcher/server/lib/license_pre_routing_factory/__tests__/license_pre_routing_factory.js +++ b/x-pack/legacy/plugins/watcher/server/np_ready/lib/license_pre_routing_factory/__tests__/license_pre_routing_factory.js @@ -5,8 +5,9 @@ */ import expect from '@kbn/expect'; +import { kibanaResponseFactory } from '../../../../../../../../../src/core/server'; import { licensePreRoutingFactory } from '../license_pre_routing_factory'; -import { LICENSE_STATUS_VALID, LICENSE_STATUS_EXPIRED } from '../../../../../../common/constants/license_status'; +import { LICENSE_STATUS_VALID, LICENSE_STATUS_EXPIRED } from '../../../../../../../common/constants/license_status'; describe('license_pre_routing_factory', () => { describe('#reportingFeaturePreRoutingFactory', () => { @@ -27,13 +28,6 @@ describe('license_pre_routing_factory', () => { }; }); - it('only instantiates one instance per server', () => { - const firstInstance = licensePreRoutingFactory(mockServer); - const secondInstance = licensePreRoutingFactory(mockServer); - - expect(firstInstance).to.be(secondInstance); - }); - describe('status is not valid', () => { beforeEach(() => { mockLicenseCheckResults = { @@ -42,13 +36,10 @@ describe('license_pre_routing_factory', () => { }); it ('replies with 403', () => { - const licensePreRouting = licensePreRoutingFactory(mockServer); + const licensePreRouting = licensePreRoutingFactory(mockServer, () => {}); const stubRequest = {}; - expect(() => licensePreRouting(stubRequest)).to.throwException((response) => { - expect(response).to.be.an(Error); - expect(response.isBoom).to.be(true); - expect(response.output.statusCode).to.be(403); - }); + const response = licensePreRouting({}, stubRequest, kibanaResponseFactory); + expect(response.status).to.be(403); }); }); @@ -60,9 +51,9 @@ describe('license_pre_routing_factory', () => { }); it ('replies with nothing', () => { - const licensePreRouting = licensePreRoutingFactory(mockServer); + const licensePreRouting = licensePreRoutingFactory(mockServer, () => null); const stubRequest = {}; - const response = licensePreRouting(stubRequest); + const response = licensePreRouting({}, stubRequest, kibanaResponseFactory); expect(response).to.be(null); }); }); diff --git a/x-pack/legacy/plugins/watcher/server/lib/license_pre_routing_factory/index.js b/x-pack/legacy/plugins/watcher/server/np_ready/lib/license_pre_routing_factory/index.ts similarity index 100% rename from x-pack/legacy/plugins/watcher/server/lib/license_pre_routing_factory/index.js rename to x-pack/legacy/plugins/watcher/server/np_ready/lib/license_pre_routing_factory/index.ts diff --git a/x-pack/legacy/plugins/watcher/server/np_ready/lib/license_pre_routing_factory/license_pre_routing_factory.ts b/x-pack/legacy/plugins/watcher/server/np_ready/lib/license_pre_routing_factory/license_pre_routing_factory.ts new file mode 100644 index 0000000000000..d2f4967246104 --- /dev/null +++ b/x-pack/legacy/plugins/watcher/server/np_ready/lib/license_pre_routing_factory/license_pre_routing_factory.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + KibanaRequest, + KibanaResponseFactory, + RequestHandler, + RequestHandlerContext, +} from 'src/core/server'; +import { PLUGIN } from '../../../../common/constants'; +import { LICENSE_STATUS_VALID } from '../../../../../../common/constants/license_status'; +import { ServerShim } from '../../types'; + +export const licensePreRoutingFactory = ( + server: ServerShim, + handler: RequestHandler +): RequestHandler => { + const xpackMainPlugin = server.plugins.xpack_main; + + // License checking and enable/disable logic + return function licensePreRouting( + ctx: RequestHandlerContext, + request: KibanaRequest, + response: KibanaResponseFactory + ) { + const licenseCheckResults = xpackMainPlugin.info.feature(PLUGIN.ID).getLicenseCheckResults(); + const { status } = licenseCheckResults; + + if (status !== LICENSE_STATUS_VALID) { + return response.customError({ + body: { + message: licenseCheckResults.messsage, + }, + statusCode: 403, + }); + } + + return handler(ctx, request, response); + }; +}; diff --git a/x-pack/legacy/plugins/watcher/server/lib/normalized_field_types/index.js b/x-pack/legacy/plugins/watcher/server/np_ready/lib/normalized_field_types/index.ts similarity index 100% rename from x-pack/legacy/plugins/watcher/server/lib/normalized_field_types/index.js rename to x-pack/legacy/plugins/watcher/server/np_ready/lib/normalized_field_types/index.ts diff --git a/x-pack/legacy/plugins/watcher/server/lib/normalized_field_types/normalized_field_types.js b/x-pack/legacy/plugins/watcher/server/np_ready/lib/normalized_field_types/normalized_field_types.ts similarity index 61% rename from x-pack/legacy/plugins/watcher/server/lib/normalized_field_types/normalized_field_types.js rename to x-pack/legacy/plugins/watcher/server/np_ready/lib/normalized_field_types/normalized_field_types.ts index 65f2867662bdd..39e82e7db8964 100644 --- a/x-pack/legacy/plugins/watcher/server/lib/normalized_field_types/normalized_field_types.js +++ b/x-pack/legacy/plugins/watcher/server/np_ready/lib/normalized_field_types/normalized_field_types.ts @@ -5,12 +5,12 @@ */ export const normalizedFieldTypes = { - 'long': 'number', - 'integer': 'number', - 'short': 'number', - 'byte': 'number', - 'double': 'number', - 'float': 'number', - 'half_float': 'number', - 'scaled_float': 'number' + long: 'number', + integer: 'number', + short: 'number', + byte: 'number', + double: 'number', + float: 'number', + half_float: 'number', + scaled_float: 'number', }; diff --git a/x-pack/legacy/plugins/watcher/server/models/action_status/__tests__/action_status.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/action_status/__tests__/action_status.js similarity index 99% rename from x-pack/legacy/plugins/watcher/server/models/action_status/__tests__/action_status.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/action_status/__tests__/action_status.js index 456768c8c02ec..430669ab26c50 100644 --- a/x-pack/legacy/plugins/watcher/server/models/action_status/__tests__/action_status.js +++ b/x-pack/legacy/plugins/watcher/server/np_ready/models/action_status/__tests__/action_status.js @@ -6,7 +6,7 @@ import expect from '@kbn/expect'; import { ActionStatus } from '../action_status'; -import { ACTION_STATES } from '../../../../common/constants'; +import { ACTION_STATES } from '../../../../../common/constants'; import moment from 'moment'; describe('action_status', () => { diff --git a/x-pack/legacy/plugins/watcher/server/models/action_status/action_status.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/action_status/action_status.js similarity index 97% rename from x-pack/legacy/plugins/watcher/server/models/action_status/action_status.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/action_status/action_status.js index eeedf9aefe5f6..7f724cf68211f 100644 --- a/x-pack/legacy/plugins/watcher/server/models/action_status/action_status.js +++ b/x-pack/legacy/plugins/watcher/server/np_ready/models/action_status/action_status.js @@ -6,8 +6,8 @@ import { get } from 'lodash'; import { badImplementation, badRequest } from 'boom'; -import { getMoment } from '../../../common/lib/get_moment'; -import { ACTION_STATES } from '../../../common/constants'; +import { getMoment } from '../../../../common/lib/get_moment'; +import { ACTION_STATES } from '../../../../common/constants'; import { i18n } from '@kbn/i18n'; export class ActionStatus { diff --git a/x-pack/legacy/plugins/watcher/server/models/action_status/index.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/action_status/index.js similarity index 100% rename from x-pack/legacy/plugins/watcher/server/models/action_status/index.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/action_status/index.js diff --git a/x-pack/legacy/plugins/watcher/server/models/execute_details/__tests__/execute_details.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/execute_details/__tests__/execute_details.js similarity index 100% rename from x-pack/legacy/plugins/watcher/server/models/execute_details/__tests__/execute_details.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/execute_details/__tests__/execute_details.js diff --git a/x-pack/legacy/plugins/watcher/server/models/execute_details/execute_details.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/execute_details/execute_details.js similarity index 100% rename from x-pack/legacy/plugins/watcher/server/models/execute_details/execute_details.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/execute_details/execute_details.js diff --git a/x-pack/legacy/plugins/watcher/server/models/execute_details/index.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/execute_details/index.js similarity index 100% rename from x-pack/legacy/plugins/watcher/server/models/execute_details/index.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/execute_details/index.js diff --git a/x-pack/legacy/plugins/watcher/server/models/fields/__tests__/fields.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/fields/__tests__/fields.js similarity index 100% rename from x-pack/legacy/plugins/watcher/server/models/fields/__tests__/fields.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/fields/__tests__/fields.js diff --git a/x-pack/legacy/plugins/watcher/server/models/fields/fields.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/fields/fields.js similarity index 100% rename from x-pack/legacy/plugins/watcher/server/models/fields/fields.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/fields/fields.js diff --git a/x-pack/legacy/plugins/watcher/server/models/fields/index.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/fields/index.js similarity index 100% rename from x-pack/legacy/plugins/watcher/server/models/fields/index.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/fields/index.js diff --git a/x-pack/legacy/plugins/watcher/server/models/settings/__tests__/settings.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/settings/__tests__/settings.js similarity index 100% rename from x-pack/legacy/plugins/watcher/server/models/settings/__tests__/settings.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/settings/__tests__/settings.js diff --git a/x-pack/legacy/plugins/watcher/server/models/settings/index.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/settings/index.js similarity index 100% rename from x-pack/legacy/plugins/watcher/server/models/settings/index.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/settings/index.js diff --git a/x-pack/legacy/plugins/watcher/server/models/settings/settings.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/settings/settings.js similarity index 97% rename from x-pack/legacy/plugins/watcher/server/models/settings/settings.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/settings/settings.js index 95a1db7533f41..55622117efedf 100644 --- a/x-pack/legacy/plugins/watcher/server/models/settings/settings.js +++ b/x-pack/legacy/plugins/watcher/server/np_ready/models/settings/settings.js @@ -5,7 +5,7 @@ */ import { merge } from 'lodash'; -import { ACTION_TYPES } from '../../../common/constants'; +import { ACTION_TYPES } from '../../../../common/constants'; function isEnabledByDefault(actionType) { switch (actionType) { diff --git a/x-pack/legacy/plugins/watcher/server/models/visualize_options/index.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/visualize_options/index.js similarity index 100% rename from x-pack/legacy/plugins/watcher/server/models/visualize_options/index.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/visualize_options/index.js diff --git a/x-pack/legacy/plugins/watcher/server/models/visualize_options/visualize_options.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/visualize_options/visualize_options.js similarity index 100% rename from x-pack/legacy/plugins/watcher/server/models/visualize_options/visualize_options.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/visualize_options/visualize_options.js diff --git a/x-pack/legacy/plugins/watcher/server/models/watch/base_watch.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/base_watch.js similarity index 98% rename from x-pack/legacy/plugins/watcher/server/models/watch/base_watch.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/watch/base_watch.js index f96274594872a..6a6df7d6f7f74 100644 --- a/x-pack/legacy/plugins/watcher/server/models/watch/base_watch.js +++ b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/base_watch.js @@ -6,7 +6,7 @@ import { get, map, pick } from 'lodash'; import { badRequest } from 'boom'; -import { Action } from '../../../common/models/action'; +import { Action } from '../../../../common/models/action'; import { WatchStatus } from '../watch_status'; import { i18n } from '@kbn/i18n'; import { WatchErrors } from '../watch_errors'; diff --git a/x-pack/legacy/plugins/watcher/server/models/watch/base_watch.test.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/base_watch.test.js similarity index 100% rename from x-pack/legacy/plugins/watcher/server/models/watch/base_watch.test.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/watch/base_watch.test.js diff --git a/x-pack/legacy/plugins/watcher/server/models/watch/index.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/index.js similarity index 100% rename from x-pack/legacy/plugins/watcher/server/models/watch/index.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/watch/index.js diff --git a/x-pack/legacy/plugins/watcher/server/models/watch/json_watch.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/json_watch.js similarity index 93% rename from x-pack/legacy/plugins/watcher/server/models/watch/json_watch.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/watch/json_watch.js index e319cc1bc277b..0b011ca33a76b 100644 --- a/x-pack/legacy/plugins/watcher/server/models/watch/json_watch.js +++ b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/json_watch.js @@ -6,8 +6,8 @@ import { isEmpty, cloneDeep, has, merge } from 'lodash'; import { BaseWatch } from './base_watch'; -import { WATCH_TYPES } from '../../../common/constants'; -import { serializeJsonWatch } from '../../../common/lib/serialization'; +import { WATCH_TYPES } from '../../../../common/constants'; +import { serializeJsonWatch } from '../../../../common/lib/serialization'; export class JsonWatch extends BaseWatch { // This constructor should not be used directly. diff --git a/x-pack/legacy/plugins/watcher/server/models/watch/json_watch.test.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/json_watch.test.js similarity index 100% rename from x-pack/legacy/plugins/watcher/server/models/watch/json_watch.test.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/watch/json_watch.test.js diff --git a/x-pack/legacy/plugins/watcher/server/models/watch/lib/get_watch_type/get_watch_type.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/lib/get_watch_type/get_watch_type.js similarity index 88% rename from x-pack/legacy/plugins/watcher/server/models/watch/lib/get_watch_type/get_watch_type.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/watch/lib/get_watch_type/get_watch_type.js index 2bdd03e23c6dc..72c725eda2bd1 100644 --- a/x-pack/legacy/plugins/watcher/server/models/watch/lib/get_watch_type/get_watch_type.js +++ b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/lib/get_watch_type/get_watch_type.js @@ -5,7 +5,7 @@ */ import { get, contains, values } from 'lodash'; -import { WATCH_TYPES } from '../../../../../common/constants'; +import { WATCH_TYPES } from '../../../../../../common/constants'; export function getWatchType(watchJson) { const type = get(watchJson, 'metadata.xpack.type'); diff --git a/x-pack/legacy/plugins/watcher/server/models/watch/lib/get_watch_type/index.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/lib/get_watch_type/index.js similarity index 100% rename from x-pack/legacy/plugins/watcher/server/models/watch/lib/get_watch_type/index.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/watch/lib/get_watch_type/index.js diff --git a/x-pack/legacy/plugins/watcher/server/models/watch/monitoring_watch.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/monitoring_watch.js similarity index 97% rename from x-pack/legacy/plugins/watcher/server/models/watch/monitoring_watch.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/watch/monitoring_watch.js index 977c62726a038..7f29d41b20fb3 100644 --- a/x-pack/legacy/plugins/watcher/server/models/watch/monitoring_watch.js +++ b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/monitoring_watch.js @@ -7,7 +7,7 @@ import { merge } from 'lodash'; import { badRequest } from 'boom'; import { BaseWatch } from './base_watch'; -import { WATCH_TYPES } from '../../../common/constants'; +import { WATCH_TYPES } from '../../../../common/constants'; import { i18n } from '@kbn/i18n'; export class MonitoringWatch extends BaseWatch { diff --git a/x-pack/legacy/plugins/watcher/server/models/watch/monitoring_watch.test.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/monitoring_watch.test.js similarity index 100% rename from x-pack/legacy/plugins/watcher/server/models/watch/monitoring_watch.test.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/watch/monitoring_watch.test.js diff --git a/x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/__tests__/format_visualize_data.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/__tests__/format_visualize_data.js similarity index 99% rename from x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/__tests__/format_visualize_data.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/__tests__/format_visualize_data.js index 04239ab6e1b5f..a7524bcc7c4db 100644 --- a/x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/__tests__/format_visualize_data.js +++ b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/__tests__/format_visualize_data.js @@ -5,7 +5,7 @@ */ import expect from '@kbn/expect'; -import { AGG_TYPES } from '../../../../../common/constants'; +import { AGG_TYPES } from '../../../../../../common/constants'; import { formatVisualizeData } from '../format_visualize_data'; describe('watch', () => { diff --git a/x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/build_visualize_query.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/build_visualize_query.js similarity index 95% rename from x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/build_visualize_query.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/build_visualize_query.js index ab9daf6f636a1..c3b73d23d96b1 100644 --- a/x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/build_visualize_query.js +++ b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/build_visualize_query.js @@ -5,8 +5,8 @@ */ import { cloneDeep } from 'lodash'; -import { buildInput } from '../../../../common/lib/serialization'; -import { AGG_TYPES } from '../../../../common/constants'; +import { buildInput } from '../../../../../common/lib/serialization'; +import { AGG_TYPES } from '../../../../../common/constants'; /* input.search.request.body.query.bool.filter.range diff --git a/x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/data_samples/count.json b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/data_samples/count.json similarity index 100% rename from x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/data_samples/count.json rename to x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/data_samples/count.json diff --git a/x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/data_samples/count.query.date.json b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/data_samples/count.query.date.json similarity index 100% rename from x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/data_samples/count.query.date.json rename to x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/data_samples/count.query.date.json diff --git a/x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/data_samples/count.query.json b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/data_samples/count.query.json similarity index 100% rename from x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/data_samples/count.query.json rename to x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/data_samples/count.query.json diff --git a/x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/data_samples/count_terms.json b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/data_samples/count_terms.json similarity index 100% rename from x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/data_samples/count_terms.json rename to x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/data_samples/count_terms.json diff --git a/x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/data_samples/count_terms.query.date.json b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/data_samples/count_terms.query.date.json similarity index 100% rename from x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/data_samples/count_terms.query.date.json rename to x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/data_samples/count_terms.query.date.json diff --git a/x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/data_samples/count_terms.query.json b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/data_samples/count_terms.query.json similarity index 100% rename from x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/data_samples/count_terms.query.json rename to x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/data_samples/count_terms.query.json diff --git a/x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/data_samples/non_count.json b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/data_samples/non_count.json similarity index 100% rename from x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/data_samples/non_count.json rename to x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/data_samples/non_count.json diff --git a/x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/data_samples/non_count.query.date.json b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/data_samples/non_count.query.date.json similarity index 100% rename from x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/data_samples/non_count.query.date.json rename to x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/data_samples/non_count.query.date.json diff --git a/x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/data_samples/non_count.query.json b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/data_samples/non_count.query.json similarity index 100% rename from x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/data_samples/non_count.query.json rename to x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/data_samples/non_count.query.json diff --git a/x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/data_samples/non_count_terms.json b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/data_samples/non_count_terms.json similarity index 100% rename from x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/data_samples/non_count_terms.json rename to x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/data_samples/non_count_terms.json diff --git a/x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/data_samples/non_count_terms.query.date.json b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/data_samples/non_count_terms.query.date.json similarity index 100% rename from x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/data_samples/non_count_terms.query.date.json rename to x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/data_samples/non_count_terms.query.date.json diff --git a/x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/data_samples/non_count_terms.query.json b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/data_samples/non_count_terms.query.json similarity index 100% rename from x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/data_samples/non_count_terms.query.json rename to x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/data_samples/non_count_terms.query.json diff --git a/x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/format_visualize_data.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/format_visualize_data.js similarity index 97% rename from x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/format_visualize_data.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/format_visualize_data.js index 90cdc9464e8c5..19d41d2491cf5 100644 --- a/x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/format_visualize_data.js +++ b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/format_visualize_data.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AGG_TYPES } from '../../../../common/constants'; +import { AGG_TYPES } from '../../../../../common/constants'; export function formatVisualizeData({ aggType, termField }, results) { if (aggType === AGG_TYPES.COUNT && !Boolean(termField)) { diff --git a/x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/index.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/index.js similarity index 100% rename from x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/index.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/index.js diff --git a/x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/threshold_watch.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/threshold_watch.js similarity index 97% rename from x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/threshold_watch.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/threshold_watch.js index cb40c46ac6435..db662902d0f4d 100644 --- a/x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/threshold_watch.js +++ b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/threshold_watch.js @@ -6,8 +6,8 @@ import { merge } from 'lodash'; import { BaseWatch } from '../base_watch'; -import { WATCH_TYPES, COMPARATORS, SORT_ORDERS } from '../../../../common/constants'; -import { serializeThresholdWatch } from '../../../../common/lib/serialization'; +import { WATCH_TYPES, COMPARATORS, SORT_ORDERS } from '../../../../../common/constants'; +import { serializeThresholdWatch } from '../../../../../common/lib/serialization'; import { buildVisualizeQuery } from './build_visualize_query'; import { formatVisualizeData } from './format_visualize_data'; diff --git a/x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/threshold_watch.test.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/threshold_watch.test.js similarity index 99% rename from x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/threshold_watch.test.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/threshold_watch.test.js index 4a0b7b657bbc6..6226a702d7f3c 100644 --- a/x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/threshold_watch.test.js +++ b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/threshold_watch.test.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { COMPARATORS, SORT_ORDERS } from '../../../../common/constants'; +import { COMPARATORS, SORT_ORDERS } from '../../../../../common/constants'; import { WatchErrors } from '../../watch_errors'; import { ThresholdWatch } from './threshold_watch'; diff --git a/x-pack/legacy/plugins/watcher/server/models/watch/watch.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/watch.js similarity index 97% rename from x-pack/legacy/plugins/watcher/server/models/watch/watch.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/watch/watch.js index c75afc62c4c4b..10b021dcbedf6 100644 --- a/x-pack/legacy/plugins/watcher/server/models/watch/watch.js +++ b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/watch.js @@ -6,7 +6,7 @@ import { set } from 'lodash'; import { badRequest } from 'boom'; -import { WATCH_TYPES } from '../../../common/constants'; +import { WATCH_TYPES } from '../../../../common/constants'; import { JsonWatch } from './json_watch'; import { MonitoringWatch } from './monitoring_watch'; import { ThresholdWatch } from './threshold_watch'; diff --git a/x-pack/legacy/plugins/watcher/server/models/watch/watch.test.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/watch.test.js similarity index 98% rename from x-pack/legacy/plugins/watcher/server/models/watch/watch.test.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/watch/watch.test.js index 2895c23083def..c419c28561730 100644 --- a/x-pack/legacy/plugins/watcher/server/models/watch/watch.test.js +++ b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/watch.test.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { WATCH_TYPES } from '../../../common/constants'; +import { WATCH_TYPES } from '../../../../common/constants'; import { Watch } from './watch'; import { JsonWatch } from './json_watch'; import { MonitoringWatch } from './monitoring_watch'; diff --git a/x-pack/legacy/plugins/watcher/server/models/watch_errors/index.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch_errors/index.js similarity index 100% rename from x-pack/legacy/plugins/watcher/server/models/watch_errors/index.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/watch_errors/index.js diff --git a/x-pack/legacy/plugins/watcher/server/models/watch_errors/watch_errors.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch_errors/watch_errors.js similarity index 100% rename from x-pack/legacy/plugins/watcher/server/models/watch_errors/watch_errors.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/watch_errors/watch_errors.js diff --git a/x-pack/legacy/plugins/watcher/server/models/watch_errors/watch_errors.test.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch_errors/watch_errors.test.js similarity index 100% rename from x-pack/legacy/plugins/watcher/server/models/watch_errors/watch_errors.test.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/watch_errors/watch_errors.test.js diff --git a/x-pack/legacy/plugins/watcher/server/models/watch_history_item/__tests__/watch_history_item.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch_history_item/__tests__/watch_history_item.js similarity index 100% rename from x-pack/legacy/plugins/watcher/server/models/watch_history_item/__tests__/watch_history_item.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/watch_history_item/__tests__/watch_history_item.js diff --git a/x-pack/legacy/plugins/watcher/server/models/watch_history_item/index.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch_history_item/index.js similarity index 100% rename from x-pack/legacy/plugins/watcher/server/models/watch_history_item/index.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/watch_history_item/index.js diff --git a/x-pack/legacy/plugins/watcher/server/models/watch_history_item/watch_history_item.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch_history_item/watch_history_item.js similarity index 97% rename from x-pack/legacy/plugins/watcher/server/models/watch_history_item/watch_history_item.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/watch_history_item/watch_history_item.js index 617f758571742..5172e590fc63e 100644 --- a/x-pack/legacy/plugins/watcher/server/models/watch_history_item/watch_history_item.js +++ b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch_history_item/watch_history_item.js @@ -5,7 +5,7 @@ */ import { badRequest } from 'boom'; -import { getMoment } from '../../../common/lib/get_moment'; +import { getMoment } from '../../../../common/lib/get_moment'; import { get, cloneDeep } from 'lodash'; import { WatchStatus } from '../watch_status'; import { i18n } from '@kbn/i18n'; diff --git a/x-pack/legacy/plugins/watcher/server/models/watch_status/__tests__/watch_status.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch_status/__tests__/watch_status.js similarity index 99% rename from x-pack/legacy/plugins/watcher/server/models/watch_status/__tests__/watch_status.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/watch_status/__tests__/watch_status.js index e29c8dd2a529e..9a045fa4b5a7f 100644 --- a/x-pack/legacy/plugins/watcher/server/models/watch_status/__tests__/watch_status.js +++ b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch_status/__tests__/watch_status.js @@ -6,7 +6,7 @@ import expect from '@kbn/expect'; import { WatchStatus } from '../watch_status'; -import { ACTION_STATES, WATCH_STATES, WATCH_STATE_COMMENTS } from '../../../../common/constants'; +import { ACTION_STATES, WATCH_STATES, WATCH_STATE_COMMENTS } from '../../../../../common/constants'; import moment from 'moment'; describe('watch_status', () => { diff --git a/x-pack/legacy/plugins/watcher/server/models/watch_status/index.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch_status/index.js similarity index 100% rename from x-pack/legacy/plugins/watcher/server/models/watch_status/index.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/watch_status/index.js diff --git a/x-pack/legacy/plugins/watcher/server/models/watch_status/watch_status.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch_status/watch_status.js similarity index 98% rename from x-pack/legacy/plugins/watcher/server/models/watch_status/watch_status.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/watch_status/watch_status.js index b7cffe16ca0bc..1e3d1d3064cb4 100644 --- a/x-pack/legacy/plugins/watcher/server/models/watch_status/watch_status.js +++ b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch_status/watch_status.js @@ -6,9 +6,9 @@ import { get, map, forEach, max } from 'lodash'; import { badRequest } from 'boom'; -import { getMoment } from '../../../common/lib/get_moment'; +import { getMoment } from '../../../../common/lib/get_moment'; import { ActionStatus } from '../action_status'; -import { ACTION_STATES, WATCH_STATES, WATCH_STATE_COMMENTS } from '../../../common/constants'; +import { ACTION_STATES, WATCH_STATES, WATCH_STATE_COMMENTS } from '../../../../common/constants'; import { i18n } from '@kbn/i18n'; function getActionStatusTotals(watchStatus) { diff --git a/x-pack/legacy/plugins/watcher/server/np_ready/plugin.ts b/x-pack/legacy/plugins/watcher/server/np_ready/plugin.ts new file mode 100644 index 0000000000000..2e8c81efa19c0 --- /dev/null +++ b/x-pack/legacy/plugins/watcher/server/np_ready/plugin.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { first } from 'rxjs/operators'; +import { Plugin, CoreSetup } from 'src/core/server'; +import { i18n } from '@kbn/i18n'; +import { PLUGIN } from '../../common/constants'; +import { ServerShim, RouteDependencies } from './types'; + +import { registerLicenseChecker } from '../../../../server/lib/register_license_checker'; +import { registerSettingsRoutes } from './routes/api/settings'; +import { registerIndicesRoutes } from './routes/api/indices'; +import { registerLicenseRoutes } from './routes/api/license'; +import { registerWatchesRoutes } from './routes/api/watches'; +import { registerWatchRoutes } from './routes/api/watch'; +import { registerListFieldsRoute } from './routes/api/register_list_fields_route'; +import { registerLoadHistoryRoute } from './routes/api/register_load_history_route'; + +export class WatcherServerPlugin implements Plugin { + async setup( + { http, elasticsearch: elasticsearchService }: CoreSetup, + { __LEGACY: serverShim }: { __LEGACY: ServerShim } + ) { + const elasticsearch = await elasticsearchService.adminClient$.pipe(first()).toPromise(); + const router = http.createRouter(); + const routeDependencies: RouteDependencies = { + elasticsearch, + elasticsearchService, + router, + }; + // Register license checker + registerLicenseChecker( + serverShim as any, + PLUGIN.ID, + PLUGIN.getI18nName(i18n), + PLUGIN.MINIMUM_LICENSE_REQUIRED + ); + + registerListFieldsRoute(routeDependencies, serverShim); + registerLoadHistoryRoute(routeDependencies, serverShim); + registerIndicesRoutes(routeDependencies, serverShim); + registerLicenseRoutes(routeDependencies, serverShim); + registerSettingsRoutes(routeDependencies, serverShim); + registerWatchesRoutes(routeDependencies, serverShim); + registerWatchRoutes(routeDependencies, serverShim); + } + start() {} + stop() {} +} diff --git a/x-pack/legacy/plugins/watcher/server/routes/api/indices/index.js b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/indices/index.ts similarity index 100% rename from x-pack/legacy/plugins/watcher/server/routes/api/indices/index.js rename to x-pack/legacy/plugins/watcher/server/np_ready/routes/api/indices/index.ts diff --git a/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/indices/register_get_route.ts b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/indices/register_get_route.ts new file mode 100644 index 0000000000000..6b6b643dc4adf --- /dev/null +++ b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/indices/register_get_route.ts @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { RequestHandler } from 'src/core/server'; +import { reduce, size } from 'lodash'; +import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; +import { isEsError } from '../../../lib/is_es_error'; +import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; +import { RouteDependencies, ServerShim } from '../../../types'; + +function getIndexNamesFromAliasesResponse(json: Record) { + return reduce( + json, + (list, { aliases }, indexName) => { + list.push(indexName); + if (size(aliases) > 0) { + list.push(...Object.keys(aliases)); + } + return list; + }, + [] as string[] + ); +} + +function getIndices(callWithRequest: any, pattern: string, limit = 10) { + return callWithRequest('indices.getAlias', { + index: pattern, + ignore: [404], + }).then((aliasResult: any) => { + if (aliasResult.status !== 404) { + const indicesFromAliasResponse = getIndexNamesFromAliasesResponse(aliasResult); + return indicesFromAliasResponse.slice(0, limit); + } + + const params = { + index: pattern, + ignore: [404], + body: { + size: 0, // no hits + aggs: { + indices: { + terms: { + field: '_index', + size: limit, + }, + }, + }, + }, + }; + + return callWithRequest('search', params).then((response: any) => { + if (response.status === 404 || !response.aggregations) { + return []; + } + return response.aggregations.indices.buckets.map((bucket: any) => bucket.key); + }); + }); +} + +export function registerGetRoute(deps: RouteDependencies, legacy: ServerShim) { + const handler: RequestHandler = async (ctx, request, response) => { + const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); + const { pattern } = request.body; + + try { + const indices = await getIndices(callWithRequest, pattern); + return response.ok({ body: { indices } }); + } catch (e) { + // Case: Error from Elasticsearch JS client + if (isEsError(e)) { + return response.customError({ statusCode: e.statusCode, body: e }); + } + + // Case: default + return response.internalError({ body: e }); + } + }; + + deps.router.post( + { + path: '/api/watcher/indices', + validate: { + body: schema.object({}, { allowUnknowns: true }), + }, + }, + licensePreRoutingFactory(legacy, handler) + ); +} diff --git a/x-pack/legacy/plugins/watcher/server/routes/api/indices/register_indices_routes.js b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/indices/register_indices_routes.ts similarity index 62% rename from x-pack/legacy/plugins/watcher/server/routes/api/indices/register_indices_routes.js rename to x-pack/legacy/plugins/watcher/server/np_ready/routes/api/indices/register_indices_routes.ts index 41b2f8dba7a1f..647a85c311532 100644 --- a/x-pack/legacy/plugins/watcher/server/routes/api/indices/register_indices_routes.js +++ b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/indices/register_indices_routes.ts @@ -5,7 +5,8 @@ */ import { registerGetRoute } from './register_get_route'; +import { RouteDependencies, ServerShim } from '../../../types'; -export function registerIndicesRoutes(server) { - registerGetRoute(server); +export function registerIndicesRoutes(deps: RouteDependencies, legacy: ServerShim) { + registerGetRoute(deps, legacy); } diff --git a/x-pack/legacy/plugins/watcher/server/routes/api/license/index.js b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/license/index.ts similarity index 100% rename from x-pack/legacy/plugins/watcher/server/routes/api/license/index.js rename to x-pack/legacy/plugins/watcher/server/np_ready/routes/api/license/index.ts diff --git a/x-pack/legacy/plugins/watcher/server/routes/api/license/register_license_routes.js b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/license/register_license_routes.ts similarity index 62% rename from x-pack/legacy/plugins/watcher/server/routes/api/license/register_license_routes.js rename to x-pack/legacy/plugins/watcher/server/np_ready/routes/api/license/register_license_routes.ts index fe890719a0a7d..c5965d9315b01 100644 --- a/x-pack/legacy/plugins/watcher/server/routes/api/license/register_license_routes.js +++ b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/license/register_license_routes.ts @@ -5,7 +5,8 @@ */ import { registerRefreshRoute } from './register_refresh_route'; +import { RouteDependencies, ServerShim } from '../../../types'; -export function registerLicenseRoutes(server) { - registerRefreshRoute(server); +export function registerLicenseRoutes(deps: RouteDependencies, legacy: ServerShim) { + registerRefreshRoute(deps, legacy); } diff --git a/x-pack/legacy/plugins/watcher/server/routes/api/license/register_refresh_route.js b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/license/register_refresh_route.ts similarity index 50% rename from x-pack/legacy/plugins/watcher/server/routes/api/license/register_refresh_route.js rename to x-pack/legacy/plugins/watcher/server/np_ready/routes/api/license/register_refresh_route.ts index cbd5dc7f6631f..08f1f26a84a4f 100644 --- a/x-pack/legacy/plugins/watcher/server/routes/api/license/register_refresh_route.js +++ b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/license/register_refresh_route.ts @@ -4,7 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { licensePreRoutingFactory } from'../../../lib/license_pre_routing_factory'; +import { RequestHandler } from 'src/core/server'; +import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; +import { RouteDependencies, ServerShim } from '../../../types'; /* In order for the client to have the most up-to-date snapshot of the current license, @@ -12,17 +14,16 @@ it needs to make a round-trip to the kibana server. This refresh endpoint is pro for when the client needs to check the license, but doesn't need to pull data from the server for any reason, i.e., when adding a new watch. */ -export function registerRefreshRoute(server) { - const licensePreRouting = licensePreRoutingFactory(server); +export function registerRefreshRoute(deps: RouteDependencies, legacy: ServerShim) { + const handler: RequestHandler = (ctx, request, response) => { + return response.ok({ body: { success: true } }); + }; - server.route({ - path: '/api/watcher/license/refresh', - method: 'GET', - handler: () => { - return { success: true }; + deps.router.get( + { + path: '/api/watcher/license/refresh', + validate: false, }, - config: { - pre: [ licensePreRouting ] - } - }); + licensePreRoutingFactory(legacy, handler) + ); } diff --git a/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/register_list_fields_route.ts b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/register_list_fields_route.ts new file mode 100644 index 0000000000000..f3222d24f0adf --- /dev/null +++ b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/register_list_fields_route.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { RequestHandler } from 'src/core/server'; +import { callWithRequestFactory } from '../../lib/call_with_request_factory'; +import { isEsError } from '../../lib/is_es_error'; +import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory'; +// @ts-ignore +import { Fields } from '../../models/fields'; +import { RouteDependencies, ServerShim } from '../../types'; + +function fetchFields(callWithRequest: any, indexes: string[]) { + const params = { + index: indexes, + fields: ['*'], + ignoreUnavailable: true, + allowNoIndices: true, + ignore: 404, + }; + + return callWithRequest('fieldCaps', params); +} + +export function registerListFieldsRoute(deps: RouteDependencies, legacy: ServerShim) { + const handler: RequestHandler = async (ctx, request, response) => { + const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); + const { indexes } = request.body; + + try { + const fieldsResponse = await fetchFields(callWithRequest, indexes); + const json = fieldsResponse.status === 404 ? { fields: [] } : fieldsResponse; + const fields = Fields.fromUpstreamJson(json); + return response.ok({ body: fields.downstreamJson }); + } catch (e) { + // Case: Error from Elasticsearch JS client + if (isEsError(e)) { + return response.customError({ + statusCode: e.statusCode, + body: { + message: e.message, + }, + }); + } + + // Case: default + return response.internalError({ body: e }); + } + }; + + deps.router.post( + { + path: '/api/watcher/fields', + validate: { + body: schema.object({ + indexes: schema.arrayOf(schema.string()), + }), + }, + }, + licensePreRoutingFactory(legacy, handler) + ); +} diff --git a/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/register_load_history_route.ts b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/register_load_history_route.ts new file mode 100644 index 0000000000000..d62e4f48c5629 --- /dev/null +++ b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/register_load_history_route.ts @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { get } from 'lodash'; +import { RequestHandler } from 'src/core/server'; +import { callWithRequestFactory } from '../../lib/call_with_request_factory'; +import { isEsError } from '../../lib/is_es_error'; +import { INDEX_NAMES } from '../../../../common/constants'; +import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory'; +import { RouteDependencies, ServerShim } from '../../types'; +// @ts-ignore +import { WatchHistoryItem } from '../../models/watch_history_item'; + +function fetchHistoryItem(callWithRequest: any, watchHistoryItemId: string) { + return callWithRequest('search', { + index: INDEX_NAMES.WATCHER_HISTORY, + body: { + query: { + bool: { + must: [{ term: { _id: watchHistoryItemId } }], + }, + }, + }, + }); +} + +export function registerLoadHistoryRoute(deps: RouteDependencies, legacy: ServerShim) { + const handler: RequestHandler = async (ctx, request, response) => { + const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); + const id = request.params.id; + + try { + const responseFromES = await fetchHistoryItem(callWithRequest, id); + const hit = get(responseFromES, 'hits.hits[0]'); + if (!hit) { + return response.notFound({ body: `Watch History Item with id = ${id} not found` }); + } + const watchHistoryItemJson = get(hit, '_source'); + const watchId = get(hit, '_source.watch_id'); + const json = { + id, + watchId, + watchHistoryItemJson, + includeDetails: true, + }; + + const watchHistoryItem = WatchHistoryItem.fromUpstreamJson(json); + return response.ok({ + body: { watchHistoryItem: watchHistoryItem.downstreamJson }, + }); + } catch (e) { + // Case: Error from Elasticsearch JS client + if (isEsError(e)) { + return response.customError({ statusCode: e.statusCode, body: e }); + } + + // Case: default + return response.internalError({ body: e }); + } + }; + + deps.router.get( + { + path: '/api/watcher/history/{id}', + validate: { + params: schema.object({ + id: schema.string(), + }), + }, + }, + licensePreRoutingFactory(legacy, handler) + ); +} diff --git a/x-pack/legacy/plugins/watcher/server/routes/api/settings/index.js b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/settings/index.ts similarity index 100% rename from x-pack/legacy/plugins/watcher/server/routes/api/settings/index.js rename to x-pack/legacy/plugins/watcher/server/np_ready/routes/api/settings/index.ts diff --git a/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/settings/register_load_route.ts b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/settings/register_load_route.ts new file mode 100644 index 0000000000000..710d079d810da --- /dev/null +++ b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/settings/register_load_route.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IClusterClient, RequestHandler } from 'src/core/server'; +import { isEsError } from '../../../lib/is_es_error'; +import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; +// @ts-ignore +import { Settings } from '../../../models/settings'; +import { RouteDependencies, ServerShim } from '../../../types'; + +function fetchClusterSettings(client: IClusterClient) { + return client.callAsInternalUser('cluster.getSettings', { + includeDefaults: true, + filterPath: '**.xpack.notification', + }); +} + +export function registerLoadRoute(deps: RouteDependencies, legacy: ServerShim) { + const handler: RequestHandler = async (ctx, request, response) => { + try { + const settings = await fetchClusterSettings(deps.elasticsearch); + return response.ok({ body: Settings.fromUpstreamJson(settings).downstreamJson }); + } catch (e) { + // Case: Error from Elasticsearch JS client + if (isEsError(e)) { + return response.customError({ statusCode: e.statusCode, body: e }); + } + + // Case: default + return response.internalError({ body: e }); + } + }; + deps.router.get( + { + path: '/api/watcher/settings', + validate: false, + }, + licensePreRoutingFactory(legacy, handler) + ); +} diff --git a/x-pack/legacy/plugins/watcher/server/routes/api/settings/register_settings_routes.js b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/settings/register_settings_routes.ts similarity index 62% rename from x-pack/legacy/plugins/watcher/server/routes/api/settings/register_settings_routes.js rename to x-pack/legacy/plugins/watcher/server/np_ready/routes/api/settings/register_settings_routes.ts index eefb320e9b1d9..0b24ec0e90bd4 100644 --- a/x-pack/legacy/plugins/watcher/server/routes/api/settings/register_settings_routes.js +++ b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/settings/register_settings_routes.ts @@ -5,7 +5,8 @@ */ import { registerLoadRoute } from './register_load_route'; +import { RouteDependencies, ServerShim } from '../../../types'; -export function registerSettingsRoutes(server) { - registerLoadRoute(server); +export function registerSettingsRoutes(deps: RouteDependencies, legacy: ServerShim) { + registerLoadRoute(deps, legacy); } diff --git a/x-pack/legacy/plugins/watcher/server/routes/api/watch/action/index.js b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/action/index.ts similarity index 100% rename from x-pack/legacy/plugins/watcher/server/routes/api/watch/action/index.js rename to x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/action/index.ts diff --git a/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/action/register_acknowledge_route.ts b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/action/register_acknowledge_route.ts new file mode 100644 index 0000000000000..d0cc0a27e87ff --- /dev/null +++ b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/action/register_acknowledge_route.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { get } from 'lodash'; +import { RequestHandler } from 'src/core/server'; +import { callWithRequestFactory } from '../../../../lib/call_with_request_factory'; +import { isEsError } from '../../../../lib/is_es_error'; +import { licensePreRoutingFactory } from '../../../../lib/license_pre_routing_factory'; +// @ts-ignore +import { WatchStatus } from '../../../../models/watch_status'; +import { RouteDependencies, ServerShim } from '../../../../types'; + +function acknowledgeAction(callWithRequest: any, watchId: string, actionId: string) { + return callWithRequest('watcher.ackWatch', { + id: watchId, + action: actionId, + }); +} + +export function registerAcknowledgeRoute(deps: RouteDependencies, legacy: ServerShim) { + const handler: RequestHandler = async (ctx, request, response) => { + const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); + const { watchId, actionId } = request.params; + + try { + const hit = await acknowledgeAction(callWithRequest, watchId, actionId); + const watchStatusJson = get(hit, 'status'); + const json = { + id: watchId, + watchStatusJson, + }; + + const watchStatus = WatchStatus.fromUpstreamJson(json); + return response.ok({ + body: { watchStatus: watchStatus.downstreamJson }, + }); + } catch (e) { + // Case: Error from Elasticsearch JS client + if (isEsError(e)) { + const body = e.statusCode === 404 ? `Watch with id = ${watchId} not found` : e; + return response.customError({ statusCode: e.statusCode, body }); + } + + // Case: default + return response.internalError({ body: e }); + } + }; + + deps.router.put( + { + path: '/api/watcher/watch/{watchId}/action/{actionId}/acknowledge', + validate: { + params: schema.object({ + watchId: schema.string(), + actionId: schema.string(), + }), + }, + }, + licensePreRoutingFactory(legacy, handler) + ); +} diff --git a/x-pack/legacy/plugins/watcher/server/routes/api/watch/action/register_action_routes.js b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/action/register_action_routes.ts similarity index 61% rename from x-pack/legacy/plugins/watcher/server/routes/api/watch/action/register_action_routes.js rename to x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/action/register_action_routes.ts index 6f2c86664420b..022c844867938 100644 --- a/x-pack/legacy/plugins/watcher/server/routes/api/watch/action/register_action_routes.js +++ b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/action/register_action_routes.ts @@ -5,7 +5,8 @@ */ import { registerAcknowledgeRoute } from './register_acknowledge_route'; +import { RouteDependencies, ServerShim } from '../../../../types'; -export function registerActionRoutes(server) { - registerAcknowledgeRoute(server); +export function registerActionRoutes(server: RouteDependencies, legacy: ServerShim) { + registerAcknowledgeRoute(server, legacy); } diff --git a/x-pack/legacy/plugins/watcher/server/routes/api/watch/index.js b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/index.ts similarity index 100% rename from x-pack/legacy/plugins/watcher/server/routes/api/watch/index.js rename to x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/index.ts diff --git a/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/register_activate_route.ts b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/register_activate_route.ts new file mode 100644 index 0000000000000..28c482124aaee --- /dev/null +++ b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/register_activate_route.ts @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { RequestHandler } from 'src/core/server'; +import { get } from 'lodash'; +import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; +import { isEsError } from '../../../lib/is_es_error'; +import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; +import { RouteDependencies, ServerShim } from '../../../types'; +// @ts-ignore +import { WatchStatus } from '../../../models/watch_status'; + +function activateWatch(callWithRequest: any, watchId: string) { + return callWithRequest('watcher.activateWatch', { + id: watchId, + }); +} + +export function registerActivateRoute(deps: RouteDependencies, legacy: ServerShim) { + const handler: RequestHandler = async (ctx, request, response) => { + const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); + + const { watchId } = request.params; + + try { + const hit = await activateWatch(callWithRequest, watchId); + const watchStatusJson = get(hit, 'status'); + const json = { + id: watchId, + watchStatusJson, + }; + + const watchStatus = WatchStatus.fromUpstreamJson(json); + return response.ok({ + body: { + watchStatus: watchStatus.downstreamJson, + }, + }); + } catch (e) { + // Case: Error from Elasticsearch JS client + if (isEsError(e)) { + const body = e.statusCode === 404 ? `Watch with id = ${watchId} not found` : e; + return response.customError({ statusCode: e.statusCode, body }); + } + + // Case: default + return response.internalError({ body: e }); + } + }; + + deps.router.put( + { + path: '/api/watcher/watch/{watchId}/activate', + validate: { + params: schema.object({ + watchId: schema.string(), + }), + }, + }, + licensePreRoutingFactory(legacy, handler) + ); +} diff --git a/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/register_deactivate_route.ts b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/register_deactivate_route.ts new file mode 100644 index 0000000000000..ac87066379a20 --- /dev/null +++ b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/register_deactivate_route.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { schema } from '@kbn/config-schema'; +import { RequestHandler } from 'src/core/server'; +import { get } from 'lodash'; +import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; +import { isEsError } from '../../../lib/is_es_error'; +import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; +import { RouteDependencies, ServerShim } from '../../../types'; +// @ts-ignore +import { WatchStatus } from '../../../models/watch_status'; + +function deactivateWatch(callWithRequest: any, watchId: string) { + return callWithRequest('watcher.deactivateWatch', { + id: watchId, + }); +} + +export function registerDeactivateRoute(deps: RouteDependencies, legacy: ServerShim) { + const handler: RequestHandler = async (ctx, request, response) => { + const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); + + const { watchId } = request.params; + + try { + const hit = await deactivateWatch(callWithRequest, watchId); + const watchStatusJson = get(hit, 'status'); + const json = { + id: watchId, + watchStatusJson, + }; + + const watchStatus = WatchStatus.fromUpstreamJson(json); + return response.ok({ + body: { + watchStatus: watchStatus.downstreamJson, + }, + }); + } catch (e) { + // Case: Error from Elasticsearch JS client + if (isEsError(e)) { + const body = e.statusCode === 404 ? `Watch with id = ${watchId} not found` : e; + return response.customError({ statusCode: e.statusCode, body }); + } + + // Case: default + return response.internalError({ body: e }); + } + }; + + deps.router.put( + { + path: '/api/watcher/watch/{watchId}/deactivate', + validate: { + params: schema.object({ + watchId: schema.string(), + }), + }, + }, + licensePreRoutingFactory(legacy, handler) + ); +} diff --git a/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/register_delete_route.ts b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/register_delete_route.ts new file mode 100644 index 0000000000000..3402cc283dba0 --- /dev/null +++ b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/register_delete_route.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { RequestHandler } from 'src/core/server'; +import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; +import { isEsError } from '../../../lib/is_es_error'; +import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; +import { RouteDependencies, ServerShim } from '../../../types'; + +function deleteWatch(callWithRequest: any, watchId: string) { + return callWithRequest('watcher.deleteWatch', { + id: watchId, + }); +} + +export function registerDeleteRoute(deps: RouteDependencies, legacy: ServerShim) { + const handler: RequestHandler = async (ctx, request, response) => { + const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); + + const { watchId } = request.params; + + try { + await deleteWatch(callWithRequest, watchId); + return response.noContent(); + } catch (e) { + // Case: Error from Elasticsearch JS client + if (isEsError(e)) { + const body = e.statusCode === 404 ? `Watch with id = ${watchId} not found` : e; + return response.customError({ statusCode: e.statusCode, body }); + } + + // Case: default + return response.internalError({ body: e }); + } + }; + + deps.router.delete( + { + path: '/api/watcher/watch/{watchId}', + validate: { + params: schema.object({ + watchId: schema.string(), + }), + }, + }, + licensePreRoutingFactory(legacy, handler) + ); +} diff --git a/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/register_execute_route.ts b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/register_execute_route.ts new file mode 100644 index 0000000000000..f3bce228653fe --- /dev/null +++ b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/register_execute_route.ts @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { RequestHandler } from 'src/core/server'; +import { get } from 'lodash'; +import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; +import { isEsError } from '../../../lib/is_es_error'; +import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; + +import { RouteDependencies, ServerShim } from '../../../types'; +// @ts-ignore +import { ExecuteDetails } from '../../../models/execute_details'; +// @ts-ignore +import { Watch } from '../../../models/watch'; +// @ts-ignore +import { WatchHistoryItem } from '../../../models/watch_history_item'; + +function executeWatch(callWithRequest: any, executeDetails: any, watchJson: any) { + const body = executeDetails; + body.watch = watchJson; + + return callWithRequest('watcher.executeWatch', { + body, + }); +} + +export function registerExecuteRoute(deps: RouteDependencies, legacy: ServerShim) { + const handler: RequestHandler = async (ctx, request, response) => { + const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); + const executeDetails = ExecuteDetails.fromDownstreamJson(request.body.executeDetails); + const watch = Watch.fromDownstreamJson(request.body.watch); + + try { + const hit = await executeWatch(callWithRequest, executeDetails.upstreamJson, watch.watchJson); + const id = get(hit, '_id'); + const watchHistoryItemJson = get(hit, 'watch_record'); + const watchId = get(hit, 'watch_record.watch_id'); + const json = { + id, + watchId, + watchHistoryItemJson, + includeDetails: true, + }; + + const watchHistoryItem = WatchHistoryItem.fromUpstreamJson(json); + return response.ok({ + body: { + watchHistoryItem: watchHistoryItem.downstreamJson, + }, + }); + } catch (e) { + // Case: Error from Elasticsearch JS client + if (isEsError(e)) { + return response.customError({ statusCode: e.statusCode, body: e }); + } + + // Case: default + return response.internalError({ body: e }); + } + }; + + deps.router.put( + { + path: '/api/watcher/watch/execute', + validate: { + body: schema.object({ + executeDetails: schema.object({}, { allowUnknowns: true }), + watch: schema.object({}, { allowUnknowns: true }), + }), + }, + }, + licensePreRoutingFactory(legacy, handler) + ); +} diff --git a/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/register_history_route.ts b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/register_history_route.ts new file mode 100644 index 0000000000000..e236d7dd642a3 --- /dev/null +++ b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/register_history_route.ts @@ -0,0 +1,97 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { RequestHandler } from 'src/core/server'; +import { get } from 'lodash'; +import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; +import { fetchAllFromScroll } from '../../../lib/fetch_all_from_scroll'; +import { INDEX_NAMES, ES_SCROLL_SETTINGS } from '../../../../../common/constants'; +import { isEsError } from '../../../lib/is_es_error'; +import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; +import { RouteDependencies, ServerShim } from '../../../types'; +// @ts-ignore +import { WatchHistoryItem } from '../../../models/watch_history_item'; + +function fetchHistoryItems(callWithRequest: any, watchId: any, startTime: any) { + const params: any = { + index: INDEX_NAMES.WATCHER_HISTORY, + scroll: ES_SCROLL_SETTINGS.KEEPALIVE, + body: { + size: ES_SCROLL_SETTINGS.PAGE_SIZE, + sort: [{ 'result.execution_time': 'desc' }], + query: { + bool: { + must: [{ term: { watch_id: watchId } }], + }, + }, + }, + }; + + // Add time range clause to query if startTime is specified + if (startTime !== 'all') { + const timeRangeQuery = { range: { 'result.execution_time': { gte: startTime } } }; + params.body.query.bool.must.push(timeRangeQuery); + } + + return callWithRequest('search', params).then((response: any) => + fetchAllFromScroll(response, callWithRequest) + ); +} + +export function registerHistoryRoute(deps: RouteDependencies, legacy: ServerShim) { + const handler: RequestHandler = async (ctx, request, response) => { + const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); + const { watchId } = request.params; + const { startTime } = request.query; + + try { + const hits = await fetchHistoryItems(callWithRequest, watchId, startTime); + const watchHistoryItems = hits.map((hit: any) => { + const id = get(hit, '_id'); + const watchHistoryItemJson = get(hit, '_source'); + + const opts = { includeDetails: false }; + return WatchHistoryItem.fromUpstreamJson( + { + id, + watchId, + watchHistoryItemJson, + }, + opts + ); + }); + + return response.ok({ + body: { + watchHistoryItems: watchHistoryItems.map( + (watchHistoryItem: any) => watchHistoryItem.downstreamJson + ), + }, + }); + } catch (e) { + // Case: Error from Elasticsearch JS client + if (isEsError(e)) { + return response.customError({ statusCode: e.statusCode, body: e }); + } + + // Case: default + return response.internalError({ body: e }); + } + }; + + deps.router.get( + { + path: '/api/watcher/watch/{watchId}/history', + validate: { + params: schema.object({ + watchId: schema.string(), + }), + }, + }, + licensePreRoutingFactory(legacy, handler) + ); +} diff --git a/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/register_load_route.ts b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/register_load_route.ts new file mode 100644 index 0000000000000..7311ad08f73a6 --- /dev/null +++ b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/register_load_route.ts @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { RequestHandler } from 'src/core/server'; +import { get } from 'lodash'; +import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; +import { isEsError } from '../../../lib/is_es_error'; +import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; +// @ts-ignore +import { Watch } from '../../../models/watch'; +import { RouteDependencies, ServerShim } from '../../../types'; + +function fetchWatch(callWithRequest: any, watchId: string) { + return callWithRequest('watcher.getWatch', { + id: watchId, + }); +} + +export function registerLoadRoute(deps: RouteDependencies, legacy: ServerShim) { + const handler: RequestHandler = async (ctx, request, response) => { + const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); + + const id = request.params.id; + + try { + const hit = await fetchWatch(callWithRequest, id); + const watchJson = get(hit, 'watch'); + const watchStatusJson = get(hit, 'status'); + const json = { + id, + watchJson, + watchStatusJson, + }; + + const watch = Watch.fromUpstreamJson(json, { + throwExceptions: { + Action: false, + }, + }); + return response.ok({ + body: { watch: watch.downstreamJson }, + }); + } catch (e) { + // Case: Error from Elasticsearch JS client + if (isEsError(e)) { + const body = e.statusCode === 404 ? `Watch with id = ${id} not found` : e; + return response.customError({ statusCode: e.statusCode, body }); + } + + // Case: default + return response.internalError({ body: e }); + } + }; + deps.router.get( + { + path: '/api/watcher/watch/{id}', + validate: { + params: schema.object({ + id: schema.string(), + }), + }, + }, + licensePreRoutingFactory(legacy, handler) + ); +} diff --git a/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/register_save_route.ts b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/register_save_route.ts new file mode 100644 index 0000000000000..5d22392d49ed8 --- /dev/null +++ b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/register_save_route.ts @@ -0,0 +1,104 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { RequestHandler } from 'src/core/server'; +import { i18n } from '@kbn/i18n'; +import { WATCH_TYPES } from '../../../../../common/constants'; +import { + serializeJsonWatch, + serializeThresholdWatch, +} from '../../../../../common/lib/serialization'; +import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; +import { isEsError } from '../../../lib/is_es_error'; +import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; +import { RouteDependencies, ServerShim } from '../../../types'; + +function fetchWatch(callWithRequest: any, watchId: string) { + return callWithRequest('watcher.getWatch', { + id: watchId, + }); +} + +function saveWatch(callWithRequest: any, id: string, body: any) { + return callWithRequest('watcher.putWatch', { + id, + body, + }); +} + +export function registerSaveRoute(deps: RouteDependencies, legacy: ServerShim) { + const handler: RequestHandler = async (ctx, request, response) => { + const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); + const { id } = request.params; + const { type, isNew, ...watchConfig } = request.body; + + // For new watches, verify watch with the same ID doesn't already exist + if (isNew) { + try { + const existingWatch = await fetchWatch(callWithRequest, id); + if (existingWatch.found) { + return response.conflict({ + body: { + message: i18n.translate('xpack.watcher.saveRoute.duplicateWatchIdErrorMessage', { + defaultMessage: "There is already a watch with ID '{watchId}'.", + values: { + watchId: id, + }, + }), + }, + }); + } + } catch (e) { + const es404 = isEsError(e) && e.statusCode === 404; + if (!es404) { + return response.internalError({ body: e }); + } + // Else continue... + } + } + + let serializedWatch; + + switch (type) { + case WATCH_TYPES.JSON: + const { name, watch } = watchConfig; + serializedWatch = serializeJsonWatch(name, watch); + break; + + case WATCH_TYPES.THRESHOLD: + serializedWatch = serializeThresholdWatch(watchConfig); + break; + } + + try { + // Create new watch + await saveWatch(callWithRequest, id, serializedWatch); + return response.noContent(); + } catch (e) { + // Case: Error from Elasticsearch JS client + if (isEsError(e)) { + return response.customError({ statusCode: e.statusCode, body: e }); + } + + // Case: default + return response.internalError({ body: e }); + } + }; + + deps.router.put( + { + path: '/api/watcher/watch/{id}', + validate: { + params: schema.object({ + id: schema.string(), + }), + body: schema.object({}, { allowUnknowns: true }), + }, + }, + licensePreRoutingFactory(legacy, handler) + ); +} diff --git a/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/register_visualize_route.ts b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/register_visualize_route.ts new file mode 100644 index 0000000000000..d07a264b0b2b1 --- /dev/null +++ b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/register_visualize_route.ts @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { RequestHandler } from 'src/core/server'; +import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; +import { isEsError } from '../../../lib/is_es_error'; +import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; +import { RouteDependencies, ServerShim } from '../../../types'; + +// @ts-ignore +import { Watch } from '../../../models/watch'; +// @ts-ignore +import { VisualizeOptions } from '../../../models/visualize_options'; + +function fetchVisualizeData(callWithRequest: any, index: any, body: any) { + const params = { + index, + body, + ignoreUnavailable: true, + allowNoIndices: true, + ignore: [404], + }; + + return callWithRequest('search', params); +} + +export function registerVisualizeRoute(deps: RouteDependencies, legacy: ServerShim) { + const handler: RequestHandler = async (ctx, request, response) => { + const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); + const watch = Watch.fromDownstreamJson(request.body.watch); + const options = VisualizeOptions.fromDownstreamJson(request.body.options); + const body = watch.getVisualizeQuery(options); + + try { + const hits = await fetchVisualizeData(callWithRequest, watch.index, body); + const visualizeData = watch.formatVisualizeData(hits); + + return response.ok({ + body: { + visualizeData, + }, + }); + } catch (e) { + // Case: Error from Elasticsearch JS client + if (isEsError(e)) { + return response.customError({ statusCode: e.statusCode, body: e }); + } + + // Case: default + return response.internalError({ body: e }); + } + }; + + deps.router.post( + { + path: '/api/watcher/watch/visualize', + validate: { + body: schema.object({ + watch: schema.object({}, { allowUnknowns: true }), + options: schema.object({}, { allowUnknowns: true }), + }), + }, + }, + licensePreRoutingFactory(legacy, handler) + ); +} diff --git a/x-pack/legacy/plugins/watcher/server/routes/api/watch/register_watch_routes.js b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/register_watch_routes.ts similarity index 62% rename from x-pack/legacy/plugins/watcher/server/routes/api/watch/register_watch_routes.js rename to x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/register_watch_routes.ts index 8419f6db7f659..5ecbf3e0d2b46 100644 --- a/x-pack/legacy/plugins/watcher/server/routes/api/watch/register_watch_routes.js +++ b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/register_watch_routes.ts @@ -13,15 +13,16 @@ import { registerActivateRoute } from './register_activate_route'; import { registerDeactivateRoute } from './register_deactivate_route'; import { registerVisualizeRoute } from './register_visualize_route'; import { registerActionRoutes } from './action'; +import { RouteDependencies, ServerShim } from '../../../types'; -export function registerWatchRoutes(server) { - registerDeleteRoute(server); - registerExecuteRoute(server); - registerLoadRoute(server); - registerSaveRoute(server); - registerHistoryRoute(server); - registerActivateRoute(server); - registerDeactivateRoute(server); - registerActionRoutes(server); - registerVisualizeRoute(server); +export function registerWatchRoutes(deps: RouteDependencies, legacy: ServerShim) { + registerDeleteRoute(deps, legacy); + registerExecuteRoute(deps, legacy); + registerLoadRoute(deps, legacy); + registerSaveRoute(deps, legacy); + registerHistoryRoute(deps, legacy); + registerActivateRoute(deps, legacy); + registerDeactivateRoute(deps, legacy); + registerActionRoutes(deps, legacy); + registerVisualizeRoute(deps, legacy); } diff --git a/x-pack/legacy/plugins/watcher/server/routes/api/watches/index.js b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watches/index.ts similarity index 100% rename from x-pack/legacy/plugins/watcher/server/routes/api/watches/index.js rename to x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watches/index.ts diff --git a/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watches/register_delete_route.ts b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watches/register_delete_route.ts new file mode 100644 index 0000000000000..29c539a0de138 --- /dev/null +++ b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watches/register_delete_route.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { RequestHandler } from 'src/core/server'; +import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; +import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; +import { RouteDependencies, ServerShim } from '../../../types'; + +function deleteWatches(callWithRequest: any, watchIds: string[]) { + const deletePromises = watchIds.map(watchId => { + return callWithRequest('watcher.deleteWatch', { + id: watchId, + }) + .then((success: Array<{ _id: string }>) => ({ success })) + .catch((error: Array<{ _id: string }>) => ({ error })); + }); + + return Promise.all(deletePromises).then(results => { + const errors: Error[] = []; + const successes: boolean[] = []; + results.forEach(({ success, error }) => { + if (success) { + successes.push(success._id); + } else if (error) { + errors.push(error._id); + } + }); + + return { + successes, + errors, + }; + }); +} + +export function registerDeleteRoute(deps: RouteDependencies, legacy: ServerShim) { + const handler: RequestHandler = async (ctx, request, response) => { + const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); + + try { + const results = await deleteWatches(callWithRequest, request.body.watchIds); + return response.ok({ body: { results } }); + } catch (e) { + return response.internalError({ body: e }); + } + }; + + deps.router.post( + { + path: '/api/watcher/watches/delete', + validate: { + body: schema.object({ + watchIds: schema.arrayOf(schema.string()), + }), + }, + }, + licensePreRoutingFactory(legacy, handler) + ); +} diff --git a/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watches/register_list_route.ts b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watches/register_list_route.ts new file mode 100644 index 0000000000000..b94c29e0f9892 --- /dev/null +++ b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watches/register_list_route.ts @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { RequestHandler } from 'src/core/server'; +import { get } from 'lodash'; +import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; +import { fetchAllFromScroll } from '../../../lib/fetch_all_from_scroll'; +import { INDEX_NAMES, ES_SCROLL_SETTINGS } from '../../../../../common/constants'; +import { isEsError } from '../../../lib/is_es_error'; +import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; +import { RouteDependencies, ServerShim } from '../../../types'; +// @ts-ignore +import { Watch } from '../../../models/watch'; + +function fetchWatches(callWithRequest: any) { + const params = { + index: INDEX_NAMES.WATCHES, + scroll: ES_SCROLL_SETTINGS.KEEPALIVE, + body: { + size: ES_SCROLL_SETTINGS.PAGE_SIZE, + }, + ignore: [404], + }; + + return callWithRequest('search', params).then((response: any) => + fetchAllFromScroll(response, callWithRequest) + ); +} + +export function registerListRoute(deps: RouteDependencies, legacy: ServerShim) { + const handler: RequestHandler = async (ctx, request, response) => { + const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); + + try { + const hits = await fetchWatches(callWithRequest); + const watches = hits.map((hit: any) => { + const id = get(hit, '_id'); + const watchJson = get(hit, '_source'); + const watchStatusJson = get(hit, '_source.status'); + + return Watch.fromUpstreamJson( + { + id, + watchJson, + watchStatusJson, + }, + { + throwExceptions: { + Action: false, + }, + } + ); + }); + + return response.ok({ + body: { + watches: watches.map((watch: any) => watch.downstreamJson), + }, + }); + } catch (e) { + // Case: Error from Elasticsearch JS client + if (isEsError(e)) { + return response.customError({ + statusCode: e.statusCode, + body: { + message: e.message, + }, + }); + } + + // Case: default + return response.internalError({ body: e }); + } + }; + + deps.router.get( + { + path: '/api/watcher/watches', + validate: false, + }, + licensePreRoutingFactory(legacy, handler) + ); +} diff --git a/x-pack/legacy/plugins/watcher/server/routes/api/watches/register_watches_routes.js b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watches/register_watches_routes.ts similarity index 62% rename from x-pack/legacy/plugins/watcher/server/routes/api/watches/register_watches_routes.js rename to x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watches/register_watches_routes.ts index 5f7ae6a5935bd..dd5f55078e591 100644 --- a/x-pack/legacy/plugins/watcher/server/routes/api/watches/register_watches_routes.js +++ b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watches/register_watches_routes.ts @@ -6,8 +6,9 @@ import { registerListRoute } from './register_list_route'; import { registerDeleteRoute } from './register_delete_route'; +import { RouteDependencies, ServerShim } from '../../../types'; -export function registerWatchesRoutes(server) { - registerListRoute(server); - registerDeleteRoute(server); +export function registerWatchesRoutes(deps: RouteDependencies, legacy: ServerShim) { + registerListRoute(deps, legacy); + registerDeleteRoute(deps, legacy); } diff --git a/x-pack/legacy/plugins/watcher/server/np_ready/types.ts b/x-pack/legacy/plugins/watcher/server/np_ready/types.ts new file mode 100644 index 0000000000000..1b566332befdf --- /dev/null +++ b/x-pack/legacy/plugins/watcher/server/np_ready/types.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IRouter, ElasticsearchServiceSetup, IClusterClient } from 'src/core/server'; +import { XPackMainPlugin } from '../../../xpack_main/xpack_main'; + +export interface ServerShim { + route: any; + plugins: { + xpack_main: XPackMainPlugin; + watcher: any; + }; +} + +export interface RouteDependencies { + router: IRouter; + elasticsearchService: ElasticsearchServiceSetup; + elasticsearch: IClusterClient; +} diff --git a/x-pack/legacy/plugins/watcher/server/routes/api/fields/register_list_route.js b/x-pack/legacy/plugins/watcher/server/routes/api/fields/register_list_route.js deleted file mode 100644 index 7d45d3a2aa60b..0000000000000 --- a/x-pack/legacy/plugins/watcher/server/routes/api/fields/register_list_route.js +++ /dev/null @@ -1,60 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; -import { isEsErrorFactory } from '../../../lib/is_es_error_factory'; -import { wrapEsError, wrapUnknownError } from '../../../lib/error_wrappers'; -import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; -import { Fields } from '../../../models/fields'; - -function fetchFields(callWithRequest, indexes) { - const params = { - index: indexes, - fields: ['*'], - ignoreUnavailable: true, - allowNoIndices: true, - ignore: 404 - }; - - return callWithRequest('fieldCaps', params); -} - -export function registerListRoute(server) { - const isEsError = isEsErrorFactory(server); - const licensePreRouting = licensePreRoutingFactory(server); - - server.route({ - path: '/api/watcher/fields', - method: 'POST', - handler: (request) => { - const callWithRequest = callWithRequestFactory(server, request); - const { indexes } = request.payload; - - return fetchFields(callWithRequest, indexes) - .then(response => { - const json = (response.status === 404) - ? { fields: [] } - : response; - - const fields = Fields.fromUpstreamJson(json); - - return fields.downstreamJson; - }) - .catch(err => { - // Case: Error from Elasticsearch JS client - if (isEsError(err)) { - throw wrapEsError(err); - } - - // Case: default - throw wrapUnknownError(err); - }); - }, - config: { - pre: [ licensePreRouting ] - } - }); -} diff --git a/x-pack/legacy/plugins/watcher/server/routes/api/history/index.js b/x-pack/legacy/plugins/watcher/server/routes/api/history/index.js deleted file mode 100644 index 9a66353c742bc..0000000000000 --- a/x-pack/legacy/plugins/watcher/server/routes/api/history/index.js +++ /dev/null @@ -1,7 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -export { registerHistoryRoutes } from './register_history_routes'; diff --git a/x-pack/legacy/plugins/watcher/server/routes/api/history/register_load_route.js b/x-pack/legacy/plugins/watcher/server/routes/api/history/register_load_route.js deleted file mode 100644 index 1d34be56fcefc..0000000000000 --- a/x-pack/legacy/plugins/watcher/server/routes/api/history/register_load_route.js +++ /dev/null @@ -1,78 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { get } from 'lodash'; -import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; -import { WatchHistoryItem } from '../../../models/watch_history_item'; -import { isEsErrorFactory } from '../../../lib/is_es_error_factory'; -import { wrapEsError, wrapUnknownError, wrapCustomError } from '../../../lib/error_wrappers'; -import { INDEX_NAMES } from '../../../../common/constants'; -import { licensePreRoutingFactory } from'../../../lib/license_pre_routing_factory'; - -function fetchHistoryItem(callWithRequest, watchHistoryItemId) { - return callWithRequest('search', { - index: INDEX_NAMES.WATCHER_HISTORY, - body: { - query: { - bool: { - must: [ - { term: { '_id': watchHistoryItemId } }, - ] - } - } - } - }); -} - -export function registerLoadRoute(server) { - const isEsError = isEsErrorFactory(server); - const licensePreRouting = licensePreRoutingFactory(server); - - server.route({ - path: '/api/watcher/history/{id}', - method: 'GET', - handler: (request) => { - const callWithRequest = callWithRequestFactory(server, request); - const id = request.params.id; - - return fetchHistoryItem(callWithRequest, id) - .then((responseFromES) => { - const hit = get(responseFromES, 'hits.hits[0]'); - if (!hit) { - throw wrapCustomError( - new Error(`Watch History Item with id = ${id} not found`), 404 - ); - } - - const watchHistoryItemJson = get(hit, '_source'); - const watchId = get(hit, '_source.watch_id'); - const json = { - id, - watchId, - watchHistoryItemJson, - includeDetails: true - }; - - const watchHistoryItem = WatchHistoryItem.fromUpstreamJson(json); - return { - watchHistoryItem: watchHistoryItem.downstreamJson - }; - }) - .catch(err => { - // Case: Error from Elasticsearch JS client - if (isEsError(err)) { - throw wrapEsError(err); - } - - // Case: default - throw wrapUnknownError(err); - }); - }, - config: { - pre: [ licensePreRouting ] - } - }); -} diff --git a/x-pack/legacy/plugins/watcher/server/routes/api/indices/register_get_route.js b/x-pack/legacy/plugins/watcher/server/routes/api/indices/register_get_route.js deleted file mode 100644 index 86de6f3da7ad5..0000000000000 --- a/x-pack/legacy/plugins/watcher/server/routes/api/indices/register_get_route.js +++ /dev/null @@ -1,89 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { reduce, size } from 'lodash'; -import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; -import { isEsErrorFactory } from '../../../lib/is_es_error_factory'; -import { wrapEsError, wrapUnknownError } from '../../../lib/error_wrappers'; -import { licensePreRoutingFactory } from'../../../lib/license_pre_routing_factory'; - -function getIndexNamesFromAliasesResponse(json) { - return reduce(json, (list, { aliases }, indexName) => { - list.push(indexName); - if (size(aliases) > 0) { - list.push(...Object.keys(aliases)); - } - return list; - }, []); -} - -function getIndices(callWithRequest, pattern, limit = 10) { - return callWithRequest('indices.getAlias', { - index: pattern, - ignore: [404] - }) - .then(aliasResult => { - if (aliasResult.status !== 404) { - const indicesFromAliasResponse = getIndexNamesFromAliasesResponse(aliasResult); - return indicesFromAliasResponse.slice(0, limit); - } - - const params = { - index: pattern, - ignore: [404], - body: { - size: 0, // no hits - aggs: { - indices: { - terms: { - field: '_index', - size: limit, - } - } - } - } - }; - - return callWithRequest('search', params) - .then(response => { - if (response.status === 404 || !response.aggregations) { - return []; - } - return response.aggregations.indices.buckets.map(bucket => bucket.key); - }); - }); -} - -export function registerGetRoute(server) { - const isEsError = isEsErrorFactory(server); - const licensePreRouting = licensePreRoutingFactory(server); - - server.route({ - path: '/api/watcher/indices', - method: 'POST', - handler: (request) => { - const callWithRequest = callWithRequestFactory(server, request); - const { pattern } = request.payload; - - return getIndices(callWithRequest, pattern) - .then(indices => { - return { indices }; - }) - .catch(err => { - // Case: Error from Elasticsearch JS client - if (isEsError(err)) { - throw wrapEsError(err); - } - - // Case: default - throw wrapUnknownError(err); - }); - }, - config: { - pre: [ licensePreRouting ] - } - }); -} diff --git a/x-pack/legacy/plugins/watcher/server/routes/api/settings/register_load_route.js b/x-pack/legacy/plugins/watcher/server/routes/api/settings/register_load_route.js deleted file mode 100644 index 65c961c8c82f2..0000000000000 --- a/x-pack/legacy/plugins/watcher/server/routes/api/settings/register_load_route.js +++ /dev/null @@ -1,47 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { callWithInternalUserFactory } from '../../../lib/call_with_internal_user_factory'; -import { isEsErrorFactory } from '../../../lib/is_es_error_factory'; -import { wrapEsError, wrapUnknownError } from '../../../lib/error_wrappers'; -import { licensePreRoutingFactory } from'../../../lib/license_pre_routing_factory'; -import { Settings } from '../../../models/settings'; - -function fetchClusterSettings(callWithInternalUser) { - return callWithInternalUser('cluster.getSettings', { - includeDefaults: true, - filterPath: '**.xpack.notification' - }); -} - -export function registerLoadRoute(server) { - const isEsError = isEsErrorFactory(server); - const licensePreRouting = licensePreRoutingFactory(server); - const callWithInternalUser = callWithInternalUserFactory(server); - - server.route({ - path: '/api/watcher/settings', - method: 'GET', - handler: () => { - return fetchClusterSettings(callWithInternalUser) - .then((settings) => { - return Settings.fromUpstreamJson(settings).downstreamJson; - }) - .catch(err => { - // Case: Error from Elasticsearch JS client - if (isEsError(err)) { - throw wrapEsError(err); - } - - // Case: default - throw wrapUnknownError(err); - }); - }, - config: { - pre: [ licensePreRouting ] - } - }); -} diff --git a/x-pack/legacy/plugins/watcher/server/routes/api/watch/action/register_acknowledge_route.js b/x-pack/legacy/plugins/watcher/server/routes/api/watch/action/register_acknowledge_route.js deleted file mode 100644 index ffecebf805cf6..0000000000000 --- a/x-pack/legacy/plugins/watcher/server/routes/api/watch/action/register_acknowledge_route.js +++ /dev/null @@ -1,63 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { get } from 'lodash'; -import { callWithRequestFactory } from '../../../../lib/call_with_request_factory'; -import { isEsErrorFactory } from '../../../../lib/is_es_error_factory'; -import { wrapEsError, wrapUnknownError } from '../../../../lib/error_wrappers'; -import { WatchStatus } from '../../../../models/watch_status'; -import { licensePreRoutingFactory } from'../../../../lib/license_pre_routing_factory'; - -export function registerAcknowledgeRoute(server) { - - const isEsError = isEsErrorFactory(server); - const licensePreRouting = licensePreRoutingFactory(server); - - server.route({ - path: '/api/watcher/watch/{watchId}/action/{actionId}/acknowledge', - method: 'PUT', - handler: (request) => { - const callWithRequest = callWithRequestFactory(server, request); - const { watchId, actionId } = request.params; - - return acknowledgeAction(callWithRequest, watchId, actionId) - .then(hit => { - const watchStatusJson = get(hit, 'status'); - const json = { - id: watchId, - watchStatusJson: watchStatusJson - }; - - const watchStatus = WatchStatus.fromUpstreamJson(json); - return { - watchStatus: watchStatus.downstreamJson - }; - }) - .catch(err => { - // Case: Error from Elasticsearch JS client - if (isEsError(err)) { - const statusCodeToMessageMap = { - 404: `Watch with id = ${watchId} not found` - }; - throw wrapEsError(err, statusCodeToMessageMap); - } - - // Case: default - throw wrapUnknownError(err); - }); - }, - config: { - pre: [ licensePreRouting ] - } - }); -} - -function acknowledgeAction(callWithRequest, watchId, actionId) { - return callWithRequest('watcher.ackWatch', { - id: watchId, - action: actionId - }); -} diff --git a/x-pack/legacy/plugins/watcher/server/routes/api/watch/register_activate_route.js b/x-pack/legacy/plugins/watcher/server/routes/api/watch/register_activate_route.js deleted file mode 100644 index ea669a16a0172..0000000000000 --- a/x-pack/legacy/plugins/watcher/server/routes/api/watch/register_activate_route.js +++ /dev/null @@ -1,63 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { get } from 'lodash'; -import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; -import { isEsErrorFactory } from '../../../lib/is_es_error_factory'; -import { wrapEsError, wrapUnknownError } from '../../../lib/error_wrappers'; -import { licensePreRoutingFactory } from'../../../lib/license_pre_routing_factory'; -import { WatchStatus } from '../../../models/watch_status'; - -function activateWatch(callWithRequest, watchId) { - return callWithRequest('watcher.activateWatch', { - id: watchId - }); -} - -export function registerActivateRoute(server) { - - const isEsError = isEsErrorFactory(server); - const licensePreRouting = licensePreRoutingFactory(server); - - server.route({ - path: '/api/watcher/watch/{watchId}/activate', - method: 'PUT', - handler: (request) => { - const callWithRequest = callWithRequestFactory(server, request); - - const { watchId } = request.params; - - return activateWatch(callWithRequest, watchId) - .then(hit => { - const watchStatusJson = get(hit, 'status'); - const json = { - id: watchId, - watchStatusJson: watchStatusJson - }; - - const watchStatus = WatchStatus.fromUpstreamJson(json); - return { - watchStatus: watchStatus.downstreamJson - }; - }) - .catch(err => { - // Case: Error from Elasticsearch JS client - if (isEsError(err)) { - const statusCodeToMessageMap = { - 404: `Watch with id = ${watchId} not found` - }; - throw wrapEsError(err, statusCodeToMessageMap); - } - - // Case: default - throw wrapUnknownError(err); - }); - }, - config: { - pre: [ licensePreRouting ] - } - }); -} diff --git a/x-pack/legacy/plugins/watcher/server/routes/api/watch/register_deactivate_route.js b/x-pack/legacy/plugins/watcher/server/routes/api/watch/register_deactivate_route.js deleted file mode 100644 index 2411290e2034a..0000000000000 --- a/x-pack/legacy/plugins/watcher/server/routes/api/watch/register_deactivate_route.js +++ /dev/null @@ -1,63 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { get } from 'lodash'; -import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; -import { isEsErrorFactory } from '../../../lib/is_es_error_factory'; -import { wrapEsError, wrapUnknownError } from '../../../lib/error_wrappers'; -import { licensePreRoutingFactory } from'../../../lib/license_pre_routing_factory'; -import { WatchStatus } from '../../../models/watch_status'; - -function deactivateWatch(callWithRequest, watchId) { - return callWithRequest('watcher.deactivateWatch', { - id: watchId - }); -} - -export function registerDeactivateRoute(server) { - - const isEsError = isEsErrorFactory(server); - const licensePreRouting = licensePreRoutingFactory(server); - - server.route({ - path: '/api/watcher/watch/{watchId}/deactivate', - method: 'PUT', - handler: (request) => { - const callWithRequest = callWithRequestFactory(server, request); - - const { watchId } = request.params; - - return deactivateWatch(callWithRequest, watchId) - .then(hit => { - const watchStatusJson = get(hit, 'status'); - const json = { - id: watchId, - watchStatusJson: watchStatusJson - }; - - const watchStatus = WatchStatus.fromUpstreamJson(json); - return { - watchStatus: watchStatus.downstreamJson - }; - }) - .catch(err => { - // Case: Error from Elasticsearch JS client - if (isEsError(err)) { - const statusCodeToMessageMap = { - 404: `Watch with id = ${watchId} not found` - }; - throw wrapEsError(err, statusCodeToMessageMap); - } - - // Case: default - throw wrapUnknownError(err); - }); - }, - config: { - pre: [ licensePreRouting ] - } - }); -} diff --git a/x-pack/legacy/plugins/watcher/server/routes/api/watch/register_delete_route.js b/x-pack/legacy/plugins/watcher/server/routes/api/watch/register_delete_route.js deleted file mode 100644 index dc3b015dffa90..0000000000000 --- a/x-pack/legacy/plugins/watcher/server/routes/api/watch/register_delete_route.js +++ /dev/null @@ -1,50 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; -import { isEsErrorFactory } from '../../../lib/is_es_error_factory'; -import { wrapEsError, wrapUnknownError } from '../../../lib/error_wrappers'; -import { licensePreRoutingFactory } from'../../../lib/license_pre_routing_factory'; - -function deleteWatch(callWithRequest, watchId) { - return callWithRequest('watcher.deleteWatch', { - id: watchId - }); -} - -export function registerDeleteRoute(server) { - - const isEsError = isEsErrorFactory(server); - const licensePreRouting = licensePreRoutingFactory(server); - - server.route({ - path: '/api/watcher/watch/{watchId}', - method: 'DELETE', - handler: (request, h) => { - const callWithRequest = callWithRequestFactory(server, request); - - const { watchId } = request.params; - - return deleteWatch(callWithRequest, watchId) - .then(() => h.response().code(204)) - .catch(err => { - // Case: Error from Elasticsearch JS client - if (isEsError(err)) { - const statusCodeToMessageMap = { - 404: `Watch with id = ${watchId} not found` - }; - throw wrapEsError(err, statusCodeToMessageMap); - } - - // Case: default - throw wrapUnknownError(err); - }); - }, - config: { - pre: [ licensePreRouting ] - } - }); -} diff --git a/x-pack/legacy/plugins/watcher/server/routes/api/watch/register_execute_route.js b/x-pack/legacy/plugins/watcher/server/routes/api/watch/register_execute_route.js deleted file mode 100644 index f378829147280..0000000000000 --- a/x-pack/legacy/plugins/watcher/server/routes/api/watch/register_execute_route.js +++ /dev/null @@ -1,69 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { get } from 'lodash'; -import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; -import { ExecuteDetails } from '../../../models/execute_details'; -import { Watch } from '../../../models/watch'; -import { WatchHistoryItem } from '../../../models/watch_history_item'; -import { isEsErrorFactory } from '../../../lib/is_es_error_factory'; -import { wrapEsError, wrapUnknownError } from '../../../lib/error_wrappers'; -import { licensePreRoutingFactory } from'../../../lib/license_pre_routing_factory'; - -function executeWatch(callWithRequest, executeDetails, watchJson) { - const body = executeDetails; - body.watch = watchJson; - - return callWithRequest('watcher.executeWatch', { - body - }); -} - -export function registerExecuteRoute(server) { - const isEsError = isEsErrorFactory(server); - const licensePreRouting = licensePreRoutingFactory(server); - - server.route({ - path: '/api/watcher/watch/execute', - method: 'PUT', - handler: (request) => { - const callWithRequest = callWithRequestFactory(server, request); - const executeDetails = ExecuteDetails.fromDownstreamJson(request.payload.executeDetails); - const watch = Watch.fromDownstreamJson(request.payload.watch); - - return executeWatch(callWithRequest, executeDetails.upstreamJson, watch.watchJson) - .then((hit) => { - const id = get(hit, '_id'); - const watchHistoryItemJson = get(hit, 'watch_record'); - const watchId = get(hit, 'watch_record.watch_id'); - const json = { - id, - watchId, - watchHistoryItemJson, - includeDetails: true - }; - - const watchHistoryItem = WatchHistoryItem.fromUpstreamJson(json); - return { - watchHistoryItem: watchHistoryItem.downstreamJson - }; - }) - .catch(err => { - - // Case: Error from Elasticsearch JS client - if (isEsError(err)) { - throw wrapEsError(err); - } - - // Case: default - throw wrapUnknownError(err); - }); - }, - config: { - pre: [ licensePreRouting ] - } - }); -} diff --git a/x-pack/legacy/plugins/watcher/server/routes/api/watch/register_history_route.js b/x-pack/legacy/plugins/watcher/server/routes/api/watch/register_history_route.js deleted file mode 100644 index 702cf8a2b64e2..0000000000000 --- a/x-pack/legacy/plugins/watcher/server/routes/api/watch/register_history_route.js +++ /dev/null @@ -1,89 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { get } from 'lodash'; -import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; -import { fetchAllFromScroll } from '../../../lib/fetch_all_from_scroll'; -import { INDEX_NAMES, ES_SCROLL_SETTINGS } from '../../../../common/constants'; -import { WatchHistoryItem } from '../../../models/watch_history_item'; -import { isEsErrorFactory } from '../../../lib/is_es_error_factory'; -import { wrapEsError, wrapUnknownError } from '../../../lib/error_wrappers'; -import { licensePreRoutingFactory } from'../../../lib/license_pre_routing_factory'; - -function fetchHistoryItems(callWithRequest, watchId, startTime) { - const params = { - index: INDEX_NAMES.WATCHER_HISTORY, - scroll: ES_SCROLL_SETTINGS.KEEPALIVE, - body: { - size: ES_SCROLL_SETTINGS.PAGE_SIZE, - sort: [ - { 'result.execution_time': 'desc' } - ], - query: { - bool: { - must: [ - { term: { 'watch_id': watchId } }, - ] - } - } - } - }; - - // Add time range clause to query if startTime is specified - if (startTime !== 'all') { - const timeRangeQuery = { range: { 'result.execution_time': { gte: startTime } } }; - params.body.query.bool.must.push(timeRangeQuery); - } - - return callWithRequest('search', params) - .then(response => fetchAllFromScroll(response, callWithRequest)); -} - -export function registerHistoryRoute(server) { - const isEsError = isEsErrorFactory(server); - const licensePreRouting = licensePreRoutingFactory(server); - - server.route({ - path: '/api/watcher/watch/{watchId}/history', - method: 'GET', - handler: (request) => { - const callWithRequest = callWithRequestFactory(server, request); - const { watchId } = request.params; - const { startTime } = request.query; - - return fetchHistoryItems(callWithRequest, watchId, startTime) - .then(hits => { - const watchHistoryItems = hits.map(hit => { - const id = get(hit, '_id'); - const watchHistoryItemJson = get(hit, '_source'); - - const opts = { includeDetails: false }; - return WatchHistoryItem.fromUpstreamJson({ - id, - watchId, - watchHistoryItemJson - }, opts); - }); - - return { - watchHistoryItems: watchHistoryItems.map(watchHistoryItem => watchHistoryItem.downstreamJson) - }; - }) - .catch(err => { - // Case: Error from Elasticsearch JS client - if (isEsError(err)) { - throw wrapEsError(err); - } - - // Case: default - throw wrapUnknownError(err); - }); - }, - config: { - pre: [ licensePreRouting ] - } - }); -} diff --git a/x-pack/legacy/plugins/watcher/server/routes/api/watch/register_load_route.js b/x-pack/legacy/plugins/watcher/server/routes/api/watch/register_load_route.js deleted file mode 100644 index e5210dbff3567..0000000000000 --- a/x-pack/legacy/plugins/watcher/server/routes/api/watch/register_load_route.js +++ /dev/null @@ -1,68 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; -import { get } from 'lodash'; -import { Watch } from '../../../models/watch'; -import { isEsErrorFactory } from '../../../lib/is_es_error_factory'; -import { wrapEsError, wrapUnknownError } from '../../../lib/error_wrappers'; -import { licensePreRoutingFactory } from'../../../lib/license_pre_routing_factory'; - -function fetchWatch(callWithRequest, watchId) { - return callWithRequest('watcher.getWatch', { - id: watchId - }); -} - -export function registerLoadRoute(server) { - const isEsError = isEsErrorFactory(server); - const licensePreRouting = licensePreRoutingFactory(server); - - server.route({ - path: '/api/watcher/watch/{id}', - method: 'GET', - handler: (request) => { - const callWithRequest = callWithRequestFactory(server, request); - - const id = request.params.id; - - return fetchWatch(callWithRequest, id) - .then(hit => { - const watchJson = get(hit, 'watch'); - const watchStatusJson = get(hit, 'status'); - const json = { - id, - watchJson, - watchStatusJson, - }; - - const watch = Watch.fromUpstreamJson(json, { - throwExceptions: { - Action: false, - }, - }); - return { - watch: watch.downstreamJson - }; - }) - .catch(err => { - // Case: Error from Elasticsearch JS client - if (isEsError(err)) { - const statusCodeToMessageMap = { - 404: `Watch with id = ${id} not found`, - }; - throw wrapEsError(err, statusCodeToMessageMap); - } - - // Case: default - throw wrapUnknownError(err); - }); - }, - config: { - pre: [ licensePreRouting ] - } - }); -} diff --git a/x-pack/legacy/plugins/watcher/server/routes/api/watch/register_save_route.js b/x-pack/legacy/plugins/watcher/server/routes/api/watch/register_save_route.js deleted file mode 100644 index 3cbb0a4e1cc47..0000000000000 --- a/x-pack/legacy/plugins/watcher/server/routes/api/watch/register_save_route.js +++ /dev/null @@ -1,94 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { WATCH_TYPES } from '../../../../common/constants'; -import { serializeJsonWatch, serializeThresholdWatch } from '../../../../common/lib/serialization'; -import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; -import { isEsErrorFactory } from '../../../lib/is_es_error_factory'; -import { wrapEsError, wrapUnknownError, wrapCustomError } from '../../../lib/error_wrappers'; -import { licensePreRoutingFactory } from'../../../lib/license_pre_routing_factory'; -import { i18n } from '@kbn/i18n'; - -function fetchWatch(callWithRequest, watchId) { - return callWithRequest('watcher.getWatch', { - id: watchId - }); -} - -function saveWatch(callWithRequest, id, body) { - return callWithRequest('watcher.putWatch', { - id, - body, - }); -} - -export function registerSaveRoute(server) { - const isEsError = isEsErrorFactory(server); - const licensePreRouting = licensePreRoutingFactory(server); - - server.route({ - path: '/api/watcher/watch/{id}', - method: 'PUT', - handler: async (request) => { - const callWithRequest = callWithRequestFactory(server, request); - const { id, type, isNew, ...watchConfig } = request.payload; - - // For new watches, verify watch with the same ID doesn't already exist - if (isNew) { - const conflictError = wrapCustomError( - new Error(i18n.translate('xpack.watcher.saveRoute.duplicateWatchIdErrorMessage', { - defaultMessage: 'There is already a watch with ID \'{watchId}\'.', - values: { - watchId: id, - } - })), - 409 - ); - - try { - const existingWatch = await fetchWatch(callWithRequest, id); - - if (existingWatch.found) { - throw conflictError; - } - } catch (e) { - // Rethrow conflict error but silently swallow all others - if (e === conflictError) { - throw e; - } - } - } - - let serializedWatch; - - switch (type) { - case WATCH_TYPES.JSON: - const { name, watch } = watchConfig; - serializedWatch = serializeJsonWatch(name, watch); - break; - - case WATCH_TYPES.THRESHOLD: - serializedWatch = serializeThresholdWatch(watchConfig); - break; - } - - // Create new watch - return saveWatch(callWithRequest, id, serializedWatch) - .catch(err => { - // Case: Error from Elasticsearch JS client - if (isEsError(err)) { - throw wrapEsError(err); - } - - // Case: default - throw wrapUnknownError(err); - }); - }, - config: { - pre: [ licensePreRouting ] - } - }); -} diff --git a/x-pack/legacy/plugins/watcher/server/routes/api/watch/register_visualize_route.js b/x-pack/legacy/plugins/watcher/server/routes/api/watch/register_visualize_route.js deleted file mode 100644 index ff9d8f9775d5e..0000000000000 --- a/x-pack/legacy/plugins/watcher/server/routes/api/watch/register_visualize_route.js +++ /dev/null @@ -1,62 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; -import { Watch } from '../../../models/watch'; -import { VisualizeOptions } from '../../../models/visualize_options'; -import { isEsErrorFactory } from '../../../lib/is_es_error_factory'; -import { wrapEsError, wrapUnknownError } from '../../../lib/error_wrappers'; -import { licensePreRoutingFactory } from'../../../lib/license_pre_routing_factory'; - -function fetchVisualizeData(callWithRequest, index, body) { - const params = { - index, - body, - ignoreUnavailable: true, - allowNoIndices: true, - ignore: [404] - }; - - return callWithRequest('search', params); -} - -export function registerVisualizeRoute(server) { - const isEsError = isEsErrorFactory(server); - const licensePreRouting = licensePreRoutingFactory(server); - - server.route({ - path: '/api/watcher/watch/visualize', - method: 'POST', - handler: (request) => { - const callWithRequest = callWithRequestFactory(server, request); - const watch = Watch.fromDownstreamJson(request.payload.watch); - const options = VisualizeOptions.fromDownstreamJson(request.payload.options); - const body = watch.getVisualizeQuery(options); - - return fetchVisualizeData(callWithRequest, watch.index, body) - .then(hits => { - const visualizeData = watch.formatVisualizeData(hits); - - return { - visualizeData - }; - }) - .catch(err => { - - // Case: Error from Elasticsearch JS client - if (isEsError(err)) { - throw wrapEsError(err); - } - - // Case: default - throw wrapUnknownError(err); - }); - }, - config: { - pre: [ licensePreRouting ] - } - }); -} diff --git a/x-pack/legacy/plugins/watcher/server/routes/api/watches/register_delete_route.js b/x-pack/legacy/plugins/watcher/server/routes/api/watches/register_delete_route.js deleted file mode 100644 index a0bbfb954b755..0000000000000 --- a/x-pack/legacy/plugins/watcher/server/routes/api/watches/register_delete_route.js +++ /dev/null @@ -1,58 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; -import { wrapUnknownError } from '../../../lib/error_wrappers'; -import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; - -function deleteWatches(callWithRequest, watchIds) { - const deletePromises = watchIds.map(watchId => { - return callWithRequest('watcher.deleteWatch', { - id: watchId, - }) - .then(success => ({ success })) - .catch(error => ({ error })); - }); - - return Promise.all(deletePromises).then(results => { - const errors = []; - const successes = []; - results.forEach(({ success, error }) => { - if (success) { - successes.push(success._id); - } else if (error) { - errors.push(error._id); - } - }); - - return { - successes, - errors, - }; - }); -} - -export function registerDeleteRoute(server) { - const licensePreRouting = licensePreRoutingFactory(server); - - server.route({ - path: '/api/watcher/watches/delete', - method: 'POST', - handler: async request => { - const callWithRequest = callWithRequestFactory(server, request); - - try { - const results = await deleteWatches(callWithRequest, request.payload.watchIds); - return { results }; - } catch (err) { - throw wrapUnknownError(err); - } - }, - config: { - pre: [licensePreRouting], - }, - }); -} diff --git a/x-pack/legacy/plugins/watcher/server/routes/api/watches/register_list_route.js b/x-pack/legacy/plugins/watcher/server/routes/api/watches/register_list_route.js deleted file mode 100644 index 2a617e275d1ee..0000000000000 --- a/x-pack/legacy/plugins/watcher/server/routes/api/watches/register_list_route.js +++ /dev/null @@ -1,79 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { get } from 'lodash'; -import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; -import { fetchAllFromScroll } from '../../../lib/fetch_all_from_scroll'; -import { INDEX_NAMES, ES_SCROLL_SETTINGS } from '../../../../common/constants'; -import { Watch } from '../../../models/watch'; -import { isEsErrorFactory } from '../../../lib/is_es_error_factory'; -import { wrapEsError, wrapUnknownError } from '../../../lib/error_wrappers'; -import { licensePreRoutingFactory } from'../../../lib/license_pre_routing_factory'; - -function fetchWatches(callWithRequest) { - const params = { - index: INDEX_NAMES.WATCHES, - scroll: ES_SCROLL_SETTINGS.KEEPALIVE, - body: { - size: ES_SCROLL_SETTINGS.PAGE_SIZE, - }, - ignore: [404] - }; - - return callWithRequest('search', params) - .then(response => fetchAllFromScroll(response, callWithRequest)); -} - -export function registerListRoute(server) { - const isEsError = isEsErrorFactory(server); - const licensePreRouting = licensePreRoutingFactory(server); - - server.route({ - path: '/api/watcher/watches', - method: 'GET', - handler: (request) => { - const callWithRequest = callWithRequestFactory(server, request); - - return fetchWatches(callWithRequest) - .then(hits => { - const watches = hits.map(hit => { - const id = get(hit, '_id'); - const watchJson = get(hit, '_source'); - const watchStatusJson = get(hit, '_source.status'); - - return Watch.fromUpstreamJson( - { - id, - watchJson, - watchStatusJson, - }, - { - throwExceptions: { - Action: false, - }, - } - ); - }); - - return { - watches: watches.map(watch => watch.downstreamJson) - }; - }) - .catch(err => { - // Case: Error from Elasticsearch JS client - if (isEsError(err)) { - throw wrapEsError(err); - } - - // Case: default - throw wrapUnknownError(err); - }); - }, - config: { - pre: [ licensePreRouting ] - } - }); -} From 248904ec87ed3dd69b5efd843ef698c3e7410ffe Mon Sep 17 00:00:00 2001 From: Robert Oskamp Date: Wed, 11 Dec 2019 11:05:36 +0100 Subject: [PATCH 19/40] [ML] API integration tests - initial tests for bucket span estimator (#52636) This PR adds basic API integration tests for the bucket span estimator. --- x-pack/test/api_integration/apis/index.js | 1 + .../apis/ml/bucket_span_estimator.ts | 90 +++++++++++++++++++ x-pack/test/api_integration/apis/ml/index.ts | 15 ++++ 3 files changed, 106 insertions(+) create mode 100644 x-pack/test/api_integration/apis/ml/bucket_span_estimator.ts create mode 100644 x-pack/test/api_integration/apis/ml/index.ts diff --git a/x-pack/test/api_integration/apis/index.js b/x-pack/test/api_integration/apis/index.js index ed0e6488320d4..fd700b41df563 100644 --- a/x-pack/test/api_integration/apis/index.js +++ b/x-pack/test/api_integration/apis/index.js @@ -28,5 +28,6 @@ export default function ({ loadTestFile }) { loadTestFile(require.resolve('./short_urls')); loadTestFile(require.resolve('./lens')); loadTestFile(require.resolve('./endpoint')); + loadTestFile(require.resolve('./ml')); }); } diff --git a/x-pack/test/api_integration/apis/ml/bucket_span_estimator.ts b/x-pack/test/api_integration/apis/ml/bucket_span_estimator.ts new file mode 100644 index 0000000000000..b5e5168621584 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/bucket_span_estimator.ts @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../ftr_provider_context'; + +const COMMON_HEADERS = { + 'kbn-xsrf': 'some-xsrf-token', +}; + +const testDataList = [ + { + testTitleSuffix: 'with 1 field, 1 agg, no split', + requestBody: { + aggTypes: ['avg'], + duration: { start: 1560297859000, end: 1562975136000 }, + fields: ['taxless_total_price'], + index: 'ecommerce', + query: { bool: { must: [{ match_all: {} }] } }, + timeField: 'order_date', + }, + expected: { + responseCode: 200, + responseBody: { name: '15m', ms: 900000 }, + }, + }, + { + testTitleSuffix: 'with 2 fields, 2 aggs, no split', + requestBody: { + aggTypes: ['avg', 'sum'], + duration: { start: 1560297859000, end: 1562975136000 }, + fields: ['products.base_price', 'products.base_unit_price'], + index: 'ecommerce', + query: { bool: { must: [{ match_all: {} }] } }, + timeField: 'order_date', + }, + expected: { + responseCode: 200, + responseBody: { name: '30m', ms: 1800000 }, + }, + }, + { + testTitleSuffix: 'with 1 field, 1 agg, 1 split with cardinality 46', + requestBody: { + aggTypes: ['avg'], + duration: { start: 1560297859000, end: 1562975136000 }, + fields: ['taxless_total_price'], + index: 'ecommerce', + query: { bool: { must: [{ match_all: {} }] } }, + splitField: 'customer_first_name.keyword', + timeField: 'order_date', + }, + expected: { + responseCode: 200, + responseBody: { name: '3h', ms: 10800000 }, + }, + }, +]; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertest'); + + describe('bucket span estimator', () => { + before(async () => { + await esArchiver.load('ml/ecommerce'); + }); + + after(async () => { + await esArchiver.unload('ml/ecommerce'); + }); + + for (const testData of testDataList) { + it(`estimates the bucket span ${testData.testTitleSuffix}`, async () => { + const { body } = await supertest + .post('/api/ml/validate/estimate_bucket_span') + .set(COMMON_HEADERS) + .send(testData.requestBody) + .expect(testData.expected.responseCode); + + expect(body).to.eql(testData.expected.responseBody); + }); + } + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/index.ts b/x-pack/test/api_integration/apis/ml/index.ts new file mode 100644 index 0000000000000..2e0521e2b8273 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function({ loadTestFile }: FtrProviderContext) { + describe('Machine Learning', function() { + this.tags(['mlqa']); + + loadTestFile(require.resolve('./bucket_span_estimator')); + }); +} From 9fcc93457f4ac382cbeb40777de369fe50c73eed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez?= Date: Wed, 11 Dec 2019 13:47:37 +0100 Subject: [PATCH 20/40] [Logs + Metrics UI] Add missing headers in Logs & metrics (#52405) * Fix broken aria references `EuiDescribedFormGroup` needs an actual header in its `title` for it to make a correct `aria-labelledby`. * Fix `aria-labelledby` references in settings page Co-authored-by: Elastic Machine --- .../fields_configuration_panel.tsx | 50 +++++++++++-------- .../indices_configuration_panel.tsx | 22 ++++---- .../name_configuration_panel.tsx | 4 +- .../analysis_setup_indices_form.tsx | 10 ++-- .../analysis_setup_timerange_form.tsx | 10 ++-- 5 files changed, 58 insertions(+), 38 deletions(-) diff --git a/x-pack/legacy/plugins/infra/public/components/source_configuration/fields_configuration_panel.tsx b/x-pack/legacy/plugins/infra/public/components/source_configuration/fields_configuration_panel.tsx index 771285e8ccee4..5f3d1a63e72eb 100644 --- a/x-pack/legacy/plugins/infra/public/components/source_configuration/fields_configuration_panel.tsx +++ b/x-pack/legacy/plugins/infra/public/components/source_configuration/fields_configuration_panel.tsx @@ -50,10 +50,12 @@ export const FieldsConfigurationPanel = ({ +

+ +

} description={ +

+ +

} description={ +

+ +

} description={ +

+ +

} description={ +

+ +

} description={ +

+ +

} description={ +

+ +

} description={ +

+ +

} description={ +

+ +

} description={ +

+ +

} description={ Date: Wed, 11 Dec 2019 13:48:40 +0100 Subject: [PATCH 21/40] [Logs + Metrics UI] Remove eslint exceptions (#50979) This removes the two eslint exceptions specific to the `infra` plugin introduced in #49244. fixes #49563 --- .eslintrc.js | 7 -- .../public/components/formatted_time.tsx | 1 - .../log_entry_actions_menu.tsx | 2 +- .../logging/log_highlights_menu.tsx | 24 +++++-- .../logging/log_text_stream/text_styles.tsx | 2 +- .../components/metrics_explorer/metrics.tsx | 33 ++++----- .../components/saved_views/create_modal.tsx | 2 +- .../add_log_column_popover.tsx | 2 +- .../source_configuration_form_state.tsx | 2 +- .../waffle/waffle_inventory_switcher.tsx | 27 ++++--- .../log_analysis_capabilities.tsx | 2 +- .../logs/log_analysis/log_analysis_module.tsx | 10 +-- .../log_analysis/log_analysis_setup_state.tsx | 2 +- .../log_highlights/log_entry_highlights.tsx | 2 +- .../log_highlights/log_summary_highlights.ts | 10 ++- .../logs/log_highlights/next_and_previous.tsx | 2 +- .../logs/log_highlights/redux_bridges.tsx | 6 +- .../containers/logs/with_stream_items.ts | 2 +- .../use_metrics_explorer_data.ts | 3 + .../use_metrics_explorer_options.ts | 2 +- .../infra/public/hooks/use_saved_view.ts | 61 ++++++++-------- .../infra/public/hooks/use_track_metric.tsx | 3 + .../public/pages/infrastructure/index.tsx | 15 ++-- .../infrastructure/metrics_explorer/index.tsx | 7 +- .../use_metric_explorer_state.ts | 12 ++-- .../logs/log_entry_rate/page_content.tsx | 2 +- .../sections/anomalies/table.tsx | 2 +- .../analysis_setup_indices_form.tsx | 2 +- .../use_log_entry_rate_module.tsx | 2 +- .../use_log_entry_rate_results_url_state.tsx | 11 +-- .../metrics/components/chart_section_vis.tsx | 18 ++--- .../metrics/components/node_details_page.tsx | 10 +-- .../pages/metrics/components/section.tsx | 71 ++++++++++--------- .../pages/metrics/components/sub_section.tsx | 36 +++++----- .../metrics/containers/with_metrics_time.tsx | 11 ++- .../infra/public/pages/metrics/index.tsx | 42 +++++------ .../infra/public/utils/cancellable_effect.ts | 3 + .../public/utils/use_kibana_ui_setting.ts | 7 +- .../infra/public/utils/use_tracked_promise.ts | 2 + .../infra/public/utils/use_url_state.ts | 38 +++++++--- .../public/utils/use_visibility_state.ts | 2 +- 41 files changed, 269 insertions(+), 231 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index e01632815bc68..367ac892107ab 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -170,13 +170,6 @@ module.exports = { 'react-hooks/rules-of-hooks': 'off', }, }, - { - files: ['x-pack/legacy/plugins/infra/**/*.{js,ts,tsx}'], - rules: { - 'react-hooks/exhaustive-deps': 'off', - 'react-hooks/rules-of-hooks': 'off', - }, - }, { files: ['x-pack/legacy/plugins/lens/**/*.{js,ts,tsx}'], rules: { diff --git a/x-pack/legacy/plugins/infra/public/components/formatted_time.tsx b/x-pack/legacy/plugins/infra/public/components/formatted_time.tsx index 78255c55df124..46b505d4fab52 100644 --- a/x-pack/legacy/plugins/infra/public/components/formatted_time.tsx +++ b/x-pack/legacy/plugins/infra/public/components/formatted_time.tsx @@ -37,7 +37,6 @@ export const useFormattedTime = ( const dateFormat = formatMap[format]; const formattedTime = useMemo(() => getFormattedTime(time, dateFormat, fallbackFormat), [ - getFormattedTime, time, dateFormat, fallbackFormat, diff --git a/x-pack/legacy/plugins/infra/public/components/logging/log_entry_flyout/log_entry_actions_menu.tsx b/x-pack/legacy/plugins/infra/public/components/logging/log_entry_flyout/log_entry_actions_menu.tsx index 92c6ddd193609..d018b3a0f38ff 100644 --- a/x-pack/legacy/plugins/infra/public/components/logging/log_entry_flyout/log_entry_actions_menu.tsx +++ b/x-pack/legacy/plugins/infra/public/components/logging/log_entry_flyout/log_entry_actions_menu.tsx @@ -51,7 +51,7 @@ export const LogEntryActionsMenu: React.FunctionComponent<{ /> , ], - [uptimeLink] + [apmLink, uptimeLink] ); const hasMenuItems = useMemo(() => menuItems.length > 0, [menuItems]); diff --git a/x-pack/legacy/plugins/infra/public/components/logging/log_highlights_menu.tsx b/x-pack/legacy/plugins/infra/public/components/logging/log_highlights_menu.tsx index 24a5e8bacb4f9..d13ccde7466cd 100644 --- a/x-pack/legacy/plugins/infra/public/components/logging/log_highlights_menu.tsx +++ b/x-pack/legacy/plugins/infra/public/components/logging/log_highlights_menu.tsx @@ -16,7 +16,7 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { debounce } from 'lodash'; -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import euiStyled from '../../../../../common/eui_styled_components'; import { useVisibilityState } from '../../utils/use_visibility_state'; @@ -47,8 +47,25 @@ export const LogHighlightsMenu: React.FC = ({ } = useVisibilityState(false); // Input field state - const [highlightTerm, setHighlightTerm] = useState(''); + const [highlightTerm, _setHighlightTerm] = useState(''); + const debouncedOnChange = useMemo(() => debounce(onChange, 275), [onChange]); + const setHighlightTerm = useCallback( + valueOrUpdater => + _setHighlightTerm(previousHighlightTerm => { + const newHighlightTerm = + typeof valueOrUpdater === 'function' + ? valueOrUpdater(previousHighlightTerm) + : valueOrUpdater; + + if (newHighlightTerm !== previousHighlightTerm) { + debouncedOnChange([newHighlightTerm]); + } + + return newHighlightTerm; + }), + [debouncedOnChange] + ); const changeHighlightTerm = useCallback( e => { const value = e.target.value; @@ -57,9 +74,6 @@ export const LogHighlightsMenu: React.FC = ({ [setHighlightTerm] ); const clearHighlightTerm = useCallback(() => setHighlightTerm(''), [setHighlightTerm]); - useEffect(() => { - debouncedOnChange([highlightTerm]); - }, [highlightTerm]); const button = ( diff --git a/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/text_styles.tsx b/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/text_styles.tsx index 1d40c88f5d1d0..e95ac6aa7923b 100644 --- a/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/text_styles.tsx +++ b/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/text_styles.tsx @@ -63,7 +63,7 @@ export const useMeasuredCharacterDimensions = (scale: TextScale) => { X ), - [scale] + [measureElement, scale] ); return { diff --git a/x-pack/legacy/plugins/infra/public/components/metrics_explorer/metrics.tsx b/x-pack/legacy/plugins/infra/public/components/metrics_explorer/metrics.tsx index d59e709d9a19a..42df7c6915a0d 100644 --- a/x-pack/legacy/plugins/infra/public/components/metrics_explorer/metrics.tsx +++ b/x-pack/legacy/plugins/infra/public/components/metrics_explorer/metrics.tsx @@ -7,7 +7,7 @@ import { EuiComboBox } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import React, { useCallback, useState, useEffect } from 'react'; +import React, { useCallback, useState } from 'react'; import { FieldType } from 'ui/index_patterns'; import { colorTransformer, MetricsExplorerColor } from '../../../common/color_palette'; import { @@ -31,24 +31,19 @@ interface SelectedOption { export const MetricsExplorerMetrics = ({ options, onChange, fields, autoFocus = false }: Props) => { const colors = Object.keys(MetricsExplorerColor) as MetricsExplorerColor[]; - const [inputRef, setInputRef] = useState(null); - const [focusOnce, setFocusState] = useState(false); + const [shouldFocus, setShouldFocus] = useState(autoFocus); - useEffect(() => { - if (inputRef && autoFocus && !focusOnce) { - inputRef.focus(); - setFocusState(true); - } - }, [inputRef]); + // the EuiCombobox forwards the ref to an input element + const autoFocusInputElement = useCallback( + (inputElement: HTMLInputElement | null) => { + if (inputElement && shouldFocus) { + inputElement.focus(); + setShouldFocus(false); + } + }, + [shouldFocus] + ); - // I tried to use useRef originally but the EUIComboBox component's type definition - // would only accept an actual input element or a callback function (with the same type). - // This effectivly does the same thing but is compatible with EuiComboBox. - const handleInputRef = (ref: HTMLInputElement) => { - if (ref) { - setInputRef(ref); - } - }; const handleChange = useCallback( selectedOptions => { onChange( @@ -59,7 +54,7 @@ export const MetricsExplorerMetrics = ({ options, onChange, fields, autoFocus = })) ); }, - [options, onChange] + [onChange, options.aggregation, colors] ); const comboOptions = fields @@ -86,7 +81,7 @@ export const MetricsExplorerMetrics = ({ options, onChange, fields, autoFocus = selectedOptions={selectedOptions} onChange={handleChange} isClearable={true} - inputRef={handleInputRef} + inputRef={autoFocusInputElement} /> ); }; diff --git a/x-pack/legacy/plugins/infra/public/components/saved_views/create_modal.tsx b/x-pack/legacy/plugins/infra/public/components/saved_views/create_modal.tsx index 8df479f36e2f9..9b8907a1ff9e1 100644 --- a/x-pack/legacy/plugins/infra/public/components/saved_views/create_modal.tsx +++ b/x-pack/legacy/plugins/infra/public/components/saved_views/create_modal.tsx @@ -36,7 +36,7 @@ export const SavedViewCreateModal = ({ close, save, isInvalid }: Props) => { const saveView = useCallback(() => { save(viewName, includeTime); - }, [viewName, includeTime]); + }, [includeTime, save, viewName]); return ( diff --git a/x-pack/legacy/plugins/infra/public/components/source_configuration/add_log_column_popover.tsx b/x-pack/legacy/plugins/infra/public/components/source_configuration/add_log_column_popover.tsx index 9b83f62e7856b..fc8407c5298e6 100644 --- a/x-pack/legacy/plugins/infra/public/components/source_configuration/add_log_column_popover.tsx +++ b/x-pack/legacy/plugins/infra/public/components/source_configuration/add_log_column_popover.tsx @@ -94,7 +94,7 @@ export const AddLogColumnButtonAndPopover: React.FunctionComponent<{ addLogColumn(selectedOption.columnConfiguration); }, - [addLogColumn, availableColumnOptions] + [addLogColumn, availableColumnOptions, closePopover] ); return ( diff --git a/x-pack/legacy/plugins/infra/public/components/source_configuration/source_configuration_form_state.tsx b/x-pack/legacy/plugins/infra/public/components/source_configuration/source_configuration_form_state.tsx index 3614a88c1e99e..262649e20709b 100644 --- a/x-pack/legacy/plugins/infra/public/components/source_configuration/source_configuration_form_state.tsx +++ b/x-pack/legacy/plugins/infra/public/components/source_configuration/source_configuration_form_state.tsx @@ -52,7 +52,7 @@ export const useSourceConfigurationFormState = (configuration?: SourceConfigurat const resetForm = useCallback(() => { indicesConfigurationFormState.resetForm(); logColumnsConfigurationFormState.resetForm(); - }, [indicesConfigurationFormState.resetForm, logColumnsConfigurationFormState.formState]); + }, [indicesConfigurationFormState, logColumnsConfigurationFormState]); const isFormDirty = useMemo( () => indicesConfigurationFormState.isFormDirty || logColumnsConfigurationFormState.isFormDirty, diff --git a/x-pack/legacy/plugins/infra/public/components/waffle/waffle_inventory_switcher.tsx b/x-pack/legacy/plugins/infra/public/components/waffle/waffle_inventory_switcher.tsx index 38e87038b7c4f..c8f03cef4d6ac 100644 --- a/x-pack/legacy/plugins/infra/public/components/waffle/waffle_inventory_switcher.tsx +++ b/x-pack/legacy/plugins/infra/public/components/waffle/waffle_inventory_switcher.tsx @@ -17,28 +17,33 @@ import { } from '../../graphql/types'; import { findInventoryModel } from '../../../common/inventory_models'; -interface Props { +interface WaffleInventorySwitcherProps { nodeType: InfraNodeType; changeNodeType: (nodeType: InfraNodeType) => void; changeGroupBy: (groupBy: InfraSnapshotGroupbyInput[]) => void; changeMetric: (metric: InfraSnapshotMetricInput) => void; } -export const WaffleInventorySwitcher = (props: Props) => { +export const WaffleInventorySwitcher: React.FC = ({ + changeNodeType, + changeGroupBy, + changeMetric, + nodeType, +}) => { const [isOpen, setIsOpen] = useState(false); const closePopover = useCallback(() => setIsOpen(false), []); const openPopover = useCallback(() => setIsOpen(true), []); const goToNodeType = useCallback( - (nodeType: InfraNodeType) => { + (targetNodeType: InfraNodeType) => { closePopover(); - props.changeNodeType(nodeType); - props.changeGroupBy([]); - const inventoryModel = findInventoryModel(nodeType); - props.changeMetric({ + changeNodeType(targetNodeType); + changeGroupBy([]); + const inventoryModel = findInventoryModel(targetNodeType); + changeMetric({ type: inventoryModel.metrics.defaultSnapshot as InfraSnapshotMetricType, }); }, - [props.changeGroupBy, props.changeNodeType, props.changeMetric] + [closePopover, changeNodeType, changeGroupBy, changeMetric] ); const goToHost = useCallback(() => goToNodeType('host' as InfraNodeType), [goToNodeType]); const goToK8 = useCallback(() => goToNodeType('pod' as InfraNodeType), [goToNodeType]); @@ -68,10 +73,10 @@ export const WaffleInventorySwitcher = (props: Props) => { ], }, ], - [] + [goToDocker, goToHost, goToK8] ); const selectedText = useMemo(() => { - switch (props.nodeType) { + switch (nodeType) { case InfraNodeType.host: return i18n.translate('xpack.infra.waffle.nodeTypeSwitcher.hostsLabel', { defaultMessage: 'Hosts', @@ -81,7 +86,7 @@ export const WaffleInventorySwitcher = (props: Props) => { case InfraNodeType.container: return 'Docker'; } - }, [props.nodeType]); + }, [nodeType]); return ( diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_capabilities.tsx b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_capabilities.tsx index 35a3ac737ada3..bb01043b0db6e 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_capabilities.tsx +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_capabilities.tsx @@ -46,7 +46,7 @@ export const useLogAnalysisCapabilities = () => { useEffect(() => { fetchMlCapabilities(); - }, []); + }, [fetchMlCapabilities]); const isLoading = useMemo(() => fetchMlCapabilitiesRequest.state === 'pending', [ fetchMlCapabilitiesRequest.state, diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_module.tsx b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_module.tsx index 189b58d7923f8..d7d0ecb6f2c8d 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_module.tsx +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_module.tsx @@ -125,23 +125,23 @@ export const useLogAnalysisModule = ({ dispatchModuleStatus({ type: 'failedSetup' }); }); }, - [cleanUpModule, setUpModule] + [cleanUpModule, dispatchModuleStatus, setUpModule] ); const viewSetupForReconfiguration = useCallback(() => { dispatchModuleStatus({ type: 'requestedJobConfigurationUpdate' }); - }, []); + }, [dispatchModuleStatus]); const viewSetupForUpdate = useCallback(() => { dispatchModuleStatus({ type: 'requestedJobDefinitionUpdate' }); - }, []); + }, [dispatchModuleStatus]); const viewResults = useCallback(() => { dispatchModuleStatus({ type: 'viewedResults' }); - }, []); + }, [dispatchModuleStatus]); const jobIds = useMemo(() => moduleDescriptor.getJobIds(spaceId, sourceId), [ - moduleDescriptor.getJobIds, + moduleDescriptor, spaceId, sourceId, ]); diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_setup_state.tsx b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_setup_state.tsx index 275c0194be3b2..74dbb3c7a8062 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_setup_state.tsx +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_setup_state.tsx @@ -140,7 +140,7 @@ export const useAnalysisSetupState = ({ ? [...errors, ...index.errors] : errors; }, []); - }, [selectedIndexNames, validatedIndices, validateIndicesRequest.state]); + }, [isValidating, validateIndicesRequest.state, selectedIndexNames, validatedIndices]); return { cleanupAndSetup, diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/log_entry_highlights.tsx b/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/log_entry_highlights.tsx index 6ead866fb960a..2b19958a9b1a1 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/log_entry_highlights.tsx +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/log_entry_highlights.tsx @@ -78,7 +78,7 @@ export const useLogEntryHighlights = ( } else { setLogEntryHighlights([]); } - }, [highlightTerms, startKey, endKey, filterQuery, sourceVersion]); + }, [endKey, filterQuery, highlightTerms, loadLogEntryHighlights, sourceVersion, startKey]); const logEntryHighlightsById = useMemo( () => diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/log_summary_highlights.ts b/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/log_summary_highlights.ts index 34c66afda010e..874c70e016496 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/log_summary_highlights.ts +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/log_summary_highlights.ts @@ -74,7 +74,15 @@ export const useLogSummaryHighlights = ( } else { setLogSummaryHighlights([]); } - }, [highlightTerms, start, end, bucketSize, filterQuery, sourceVersion]); + }, [ + bucketSize, + debouncedLoadSummaryHighlights, + end, + filterQuery, + highlightTerms, + sourceVersion, + start, + ]); return { logSummaryHighlights, diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/next_and_previous.tsx b/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/next_and_previous.tsx index 95ead50119eb4..62a43a5412825 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/next_and_previous.tsx +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/next_and_previous.tsx @@ -53,7 +53,7 @@ export const useNextAndPrevious = ({ const initialTimeKey = getUniqueLogEntryKey(entries[initialIndex]); setCurrentTimeKey(initialTimeKey); } - }, [currentTimeKey, entries, setCurrentTimeKey]); + }, [currentTimeKey, entries, setCurrentTimeKey, visibleMidpoint]); const indexOfCurrentTimeKey = useMemo(() => { if (currentTimeKey && entries.length > 0) { diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/redux_bridges.tsx b/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/redux_bridges.tsx index 2b60c6edd97aa..9ea8987d4f326 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/redux_bridges.tsx +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/redux_bridges.tsx @@ -25,11 +25,11 @@ export const LogHighlightsPositionBridge = withLogPosition( const { setJumpToTarget, setVisibleMidpoint } = useContext(LogHighlightsState.Context); useEffect(() => { setVisibleMidpoint(visibleMidpoint); - }, [visibleMidpoint]); + }, [setVisibleMidpoint, visibleMidpoint]); useEffect(() => { setJumpToTarget(() => jumpToTargetPosition); - }, [jumpToTargetPosition]); + }, [jumpToTargetPosition, setJumpToTarget]); return null; } @@ -41,7 +41,7 @@ export const LogHighlightsFilterQueryBridge = withLogFilter( useEffect(() => { setFilterQuery(serializedFilterQuery); - }, [serializedFilterQuery]); + }, [serializedFilterQuery, setFilterQuery]); return null; } diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/with_stream_items.ts b/x-pack/legacy/plugins/infra/public/containers/logs/with_stream_items.ts index da468b4391e4e..9b20676486af2 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/with_stream_items.ts +++ b/x-pack/legacy/plugins/infra/public/containers/logs/with_stream_items.ts @@ -35,7 +35,7 @@ export const WithStreamItems: React.FunctionComponent<{ createLogEntryStreamItem(logEntry, logEntryHighlightsById[logEntry.gid] || []) ), - [logEntries.entries, logEntryHighlightsById] + [isAutoReloading, logEntries.entries, logEntries.isReloading, logEntryHighlightsById] ); return children({ diff --git a/x-pack/legacy/plugins/infra/public/containers/metrics_explorer/use_metrics_explorer_data.ts b/x-pack/legacy/plugins/infra/public/containers/metrics_explorer/use_metrics_explorer_data.ts index 1418d6aef67ac..c2a599ea1ae78 100644 --- a/x-pack/legacy/plugins/infra/public/containers/metrics_explorer/use_metrics_explorer_data.ts +++ b/x-pack/legacy/plugins/infra/public/containers/metrics_explorer/use_metrics_explorer_data.ts @@ -96,6 +96,9 @@ export function useMetricsExplorerData( } setLoading(false); })(); + + // TODO: fix this dependency list while preserving the semantics + // eslint-disable-next-line react-hooks/exhaustive-deps }, [options, source, timerange, signal, afterKey]); return { error, loading, data }; } diff --git a/x-pack/legacy/plugins/infra/public/containers/metrics_explorer/use_metrics_explorer_options.ts b/x-pack/legacy/plugins/infra/public/containers/metrics_explorer/use_metrics_explorer_options.ts index 278f3e0a9c17d..de7a8d5805ecc 100644 --- a/x-pack/legacy/plugins/infra/public/containers/metrics_explorer/use_metrics_explorer_options.ts +++ b/x-pack/legacy/plugins/infra/public/containers/metrics_explorer/use_metrics_explorer_options.ts @@ -102,7 +102,7 @@ function useStateWithLocalStorage( const [state, setState] = useState(parseJsonOrDefault(storageState, defaultState)); useEffect(() => { localStorage.setItem(key, JSON.stringify(state)); - }, [state]); + }, [key, state]); return [state, setState]; } diff --git a/x-pack/legacy/plugins/infra/public/hooks/use_saved_view.ts b/x-pack/legacy/plugins/infra/public/hooks/use_saved_view.ts index 8db0ed28d9b21..4b12b6c51ea0e 100644 --- a/x-pack/legacy/plugins/infra/public/hooks/use_saved_view.ts +++ b/x-pack/legacy/plugins/infra/public/hooks/use_saved_view.ts @@ -26,29 +26,32 @@ export const useSavedView = (defaultViewState: ViewState, viewType: s >(viewType); const { create, error: errorOnCreate, createdId } = useCreateSavedObject(viewType); const { deleteObject, deletedId } = useDeleteSavedObject(viewType); - const deleteView = useCallback((id: string) => deleteObject(id), []); + const deleteView = useCallback((id: string) => deleteObject(id), [deleteObject]); const [createError, setCreateError] = useState(null); - useEffect(() => setCreateError(createError), [errorOnCreate, setCreateError]); + useEffect(() => setCreateError(errorOnCreate), [errorOnCreate]); - const saveView = useCallback((d: { [p: string]: any }) => { - const doSave = async () => { - const exists = await hasView(d.name); - if (exists) { - setCreateError( - i18n.translate('xpack.infra.savedView.errorOnCreate.duplicateViewName', { - defaultMessage: `A view with that name already exists.`, - }) - ); - return; - } - create(d); - }; - setCreateError(null); - doSave(); - }, []); + const saveView = useCallback( + (d: { [p: string]: any }) => { + const doSave = async () => { + const exists = await hasView(d.name); + if (exists) { + setCreateError( + i18n.translate('xpack.infra.savedView.errorOnCreate.duplicateViewName', { + defaultMessage: `A view with that name already exists.`, + }) + ); + return; + } + create(d); + }; + setCreateError(null); + doSave(); + }, + [create, hasView] + ); - const savedObjects = data ? data.savedObjects : []; + const savedObjects = useMemo(() => (data ? data.savedObjects : []), [data]); const views = useMemo(() => { const items: Array> = [ { @@ -61,19 +64,17 @@ export const useSavedView = (defaultViewState: ViewState, viewType: s }, ]; - if (data) { - data.savedObjects.forEach( - o => - o.type === viewType && - items.push({ - ...o.attributes, - id: o.id, - }) - ); - } + savedObjects.forEach( + o => + o.type === viewType && + items.push({ + ...o.attributes, + id: o.id, + }) + ); return items; - }, [savedObjects, defaultViewState]); + }, [defaultViewState, savedObjects, viewType]); return { views, diff --git a/x-pack/legacy/plugins/infra/public/hooks/use_track_metric.tsx b/x-pack/legacy/plugins/infra/public/hooks/use_track_metric.tsx index 379b3af3f1063..c5945ab808202 100644 --- a/x-pack/legacy/plugins/infra/public/hooks/use_track_metric.tsx +++ b/x-pack/legacy/plugins/infra/public/hooks/use_track_metric.tsx @@ -57,6 +57,9 @@ export function useTrackMetric( const trackUiMetric = getTrackerForApp(app); const id = setTimeout(() => trackUiMetric(metricType, decoratedMetric), Math.max(delay, 0)); return () => clearTimeout(id); + + // the dependencies are managed externally + // eslint-disable-next-line react-hooks/exhaustive-deps }, effectDependencies); } diff --git a/x-pack/legacy/plugins/infra/public/pages/infrastructure/index.tsx b/x-pack/legacy/plugins/infra/public/pages/infrastructure/index.tsx index fe48fcc62f77d..9efbbe790abc1 100644 --- a/x-pack/legacy/plugins/infra/public/pages/infrastructure/index.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/infrastructure/index.tsx @@ -24,6 +24,7 @@ import { MetricsExplorerPage } from './metrics_explorer'; import { SnapshotPage } from './snapshot'; import { SettingsPage } from '../shared/settings'; import { AppNavigation } from '../../components/navigation/app_navigation'; +import { SourceLoadingPage } from '../../components/source_loading_page'; interface InfrastructurePageProps extends RouteComponentProps { uiCapabilities: UICapabilities; @@ -95,11 +96,15 @@ export const InfrastructurePage = injectUICapabilities( {({ configuration, createDerivedIndexPattern }) => ( - + {configuration ? ( + + ) : ( + + )} )} diff --git a/x-pack/legacy/plugins/infra/public/pages/infrastructure/metrics_explorer/index.tsx b/x-pack/legacy/plugins/infra/public/pages/infrastructure/metrics_explorer/index.tsx index 63f5a81967618..4db4319b91d3c 100644 --- a/x-pack/legacy/plugins/infra/public/pages/infrastructure/metrics_explorer/index.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/infrastructure/metrics_explorer/index.tsx @@ -11,22 +11,17 @@ import { IIndexPattern } from 'src/plugins/data/public'; import { DocumentTitle } from '../../../components/document_title'; import { MetricsExplorerCharts } from '../../../components/metrics_explorer/charts'; import { MetricsExplorerToolbar } from '../../../components/metrics_explorer/toolbar'; -import { SourceLoadingPage } from '../../../components/source_loading_page'; import { SourceQuery } from '../../../../common/graphql/types'; import { NoData } from '../../../components/empty_states'; import { useMetricsExplorerState } from './use_metric_explorer_state'; import { useTrackPageview } from '../../../hooks/use_track_metric'; interface MetricsExplorerPageProps { - source: SourceQuery.Query['source']['configuration'] | undefined; + source: SourceQuery.Query['source']['configuration']; derivedIndexPattern: IIndexPattern; } export const MetricsExplorerPage = ({ source, derivedIndexPattern }: MetricsExplorerPageProps) => { - if (!source) { - return ; - } - const { loading, error, diff --git a/x-pack/legacy/plugins/infra/public/pages/infrastructure/metrics_explorer/use_metric_explorer_state.ts b/x-pack/legacy/plugins/infra/public/pages/infrastructure/metrics_explorer/use_metric_explorer_state.ts index 415a6ae89a8b1..57ea886169701 100644 --- a/x-pack/legacy/plugins/infra/public/pages/infrastructure/metrics_explorer/use_metric_explorer_state.ts +++ b/x-pack/legacy/plugins/infra/public/pages/infrastructure/metrics_explorer/use_metric_explorer_state.ts @@ -59,7 +59,7 @@ export const useMetricsExplorerState = ( setAfterKey(null); setTimeRange({ ...currentTimerange, from: start, to: end }); }, - [currentTimerange] + [currentTimerange, setTimeRange] ); const handleGroupByChange = useCallback( @@ -70,7 +70,7 @@ export const useMetricsExplorerState = ( groupBy: groupBy || void 0, }); }, - [options] + [options, setOptions] ); const handleFilterQuerySubmit = useCallback( @@ -81,7 +81,7 @@ export const useMetricsExplorerState = ( filterQuery: query, }); }, - [options] + [options, setOptions] ); const handleMetricsChange = useCallback( @@ -92,7 +92,7 @@ export const useMetricsExplorerState = ( metrics, }); }, - [options] + [options, setOptions] ); const handleAggregationChange = useCallback( @@ -109,7 +109,7 @@ export const useMetricsExplorerState = ( })); setOptions({ ...options, aggregation, metrics }); }, - [options] + [options, setOptions] ); const onViewStateChange = useCallback( @@ -124,7 +124,7 @@ export const useMetricsExplorerState = ( setOptions(vs.options); } }, - [setChartOptions, setTimeRange, setTimeRange] + [setChartOptions, setOptions, setTimeRange] ); return { diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/page_content.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/page_content.tsx index e62164cb17b2c..e71985f73fbb8 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/page_content.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/page_content.tsx @@ -36,7 +36,7 @@ export const LogEntryRatePageContent = () => { useEffect(() => { fetchModuleDefinition(); fetchJobStatus(); - }, []); + }, [fetchJobStatus, fetchModuleDefinition]); if (!hasLogAnalysisCapabilites) { return ; diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/table.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/table.tsx index 2057d75f72354..86760cf2da7d6 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/table.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/table.tsx @@ -124,7 +124,7 @@ export const AnomaliesTable: React.FunctionComponent<{ setItemIdToExpandedRowMap(newItemIdToExpandedRowMap); } }, - [results, setTimeRange, timeRange, itemIdToExpandedRowMap, setItemIdToExpandedRowMap] + [itemIdToExpandedRowMap, jobId, results, setTimeRange, timeRange] ); const columns = [ diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/setup/initial_configuration_step/analysis_setup_indices_form.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/setup/initial_configuration_step/analysis_setup_indices_form.tsx index 35cad040323a6..5a4c21670191e 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/setup/initial_configuration_step/analysis_setup_indices_form.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/setup/initial_configuration_step/analysis_setup_indices_form.tsx @@ -54,7 +54,7 @@ export const AnalysisSetupIndicesForm: React.FunctionComponent<{
); }), - [indices] + [handleCheckboxChange, indices] ); return ( diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_rate_module.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_rate_module.tsx index ab6a6578601bf..d1efedb176aba 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_rate_module.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_rate_module.tsx @@ -31,7 +31,7 @@ export const useLogEntryRateModule = ({ spaceId, timestampField, }), - [indexPattern] + [indexPattern, sourceId, spaceId, timestampField] ); return useLogAnalysisModule({ diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_rate_results_url_state.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_rate_results_url_state.tsx index 017be6be49e16..6d4495c8d9e0f 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_rate_results_url_state.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_rate_results_url_state.tsx @@ -8,7 +8,6 @@ import { fold } from 'fp-ts/lib/Either'; import { constant, identity } from 'fp-ts/lib/function'; import { pipe } from 'fp-ts/lib/pipeable'; import * as rt from 'io-ts'; -import { useEffect } from 'react'; import { useUrlState } from '../../../utils/use_url_state'; @@ -41,12 +40,9 @@ export const useLogAnalysisResultsUrlState = () => { pipe(urlTimeRangeRT.decode(value), fold(constant(undefined), identity)), encodeUrlState: urlTimeRangeRT.encode, urlStateKey: TIME_RANGE_URL_STATE_KEY, + writeDefaultState: true, }); - useEffect(() => { - setTimeRange(timeRange); - }, []); - const [autoRefresh, setAutoRefresh] = useUrlState({ defaultState: { isPaused: false, @@ -56,12 +52,9 @@ export const useLogAnalysisResultsUrlState = () => { pipe(autoRefreshRT.decode(value), fold(constant(undefined), identity)), encodeUrlState: autoRefreshRT.encode, urlStateKey: AUTOREFRESH_URL_STATE_KEY, + writeDefaultState: true, }); - useEffect(() => { - setAutoRefresh(autoRefresh); - }, []); - return { timeRange, setTimeRange, diff --git a/x-pack/legacy/plugins/infra/public/pages/metrics/components/chart_section_vis.tsx b/x-pack/legacy/plugins/infra/public/pages/metrics/components/chart_section_vis.tsx index 425b5a43f793f..309961cc39025 100644 --- a/x-pack/legacy/plugins/infra/public/pages/metrics/components/chart_section_vis.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/metrics/components/chart_section_vis.tsx @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback } from 'react'; +import React, { useCallback, useMemo } from 'react'; import moment from 'moment'; import { i18n } from '@kbn/i18n'; import { @@ -42,15 +42,15 @@ export const ChartSectionVis = ({ seriesOverrides, type, }: VisSectionProps) => { - if (!metric || !id) { - return null; - } const [dateFormat] = useKibanaUiSetting('dateFormat'); const valueFormatter = useCallback(getFormatter(formatter, formatterTemplate), [ formatter, formatterTemplate, ]); - const dateFormatter = useCallback(niceTimeFormatter(getMaxMinTimestamp(metric)), [metric]); + const dateFormatter = useMemo( + () => (metric != null ? niceTimeFormatter(getMaxMinTimestamp(metric)) : undefined), + [metric] + ); const handleTimeChange = useCallback( (from: number, to: number) => { if (onChangeRangeTime) { @@ -73,7 +73,9 @@ export const ChartSectionVis = ({ ), }; - if (!metric) { + if (!id) { + return null; + } else if (!metric) { return ( ); - } - - if (metric.series.some(seriesHasLessThen2DataPoints)) { + } else if (metric.series.some(seriesHasLessThen2DataPoints)) { return ( { - if (!props.metadata) { - return null; - } - const { parsedTimeRange } = props; const { metrics, loading, makeRequest, error } = useNodeDetails( props.requiredMetrics, @@ -65,11 +61,11 @@ export const NodeDetailsPage = (props: Props) => { const refetch = useCallback(() => { makeRequest(); - }, []); + }, [makeRequest]); useEffect(() => { makeRequest(); - }, [parsedTimeRange]); + }, [makeRequest, parsedTimeRange]); if (error) { return ; diff --git a/x-pack/legacy/plugins/infra/public/pages/metrics/components/section.tsx b/x-pack/legacy/plugins/infra/public/pages/metrics/components/section.tsx index 32d2e2eff8ab9..2f9ed9f54df82 100644 --- a/x-pack/legacy/plugins/infra/public/pages/metrics/components/section.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/metrics/components/section.tsx @@ -4,15 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ +import { EuiTitle } from '@elastic/eui'; import React, { - useContext, Children, - isValidElement, cloneElement, FunctionComponent, - useMemo, + isValidElement, + useContext, } from 'react'; -import { EuiTitle } from '@elastic/eui'; + import { SideNavContext, SubNavItem } from '../lib/side_nav_context'; import { LayoutProps } from '../types'; @@ -31,35 +31,42 @@ export const Section: FunctionComponent = ({ stopLiveStreaming, }) => { const { addNavItem } = useContext(SideNavContext); - const subNavItems: SubNavItem[] = []; - const childrenWithProps = useMemo( - () => - Children.map(children, child => { - if (isValidElement(child)) { - const metric = (metrics && metrics.find(m => m.id === child.props.id)) || null; - if (metric) { - subNavItems.push({ - id: child.props.id, - name: child.props.label, - onClick: () => { - const el = document.getElementById(child.props.id); - if (el) { - el.scrollIntoView(); - } - }, - }); - } - return cloneElement(child, { - metrics, - onChangeRangeTime, - isLiveStreaming, - stopLiveStreaming, - }); - } - return null; - }), - [children, metrics, onChangeRangeTime, isLiveStreaming, stopLiveStreaming] + const subNavItems = Children.toArray(children).reduce( + (accumulatedChildren, child) => { + if (!isValidElement(child)) { + return accumulatedChildren; + } + const metric = metrics?.find(m => m.id === child.props.id) ?? null; + if (metric === null) { + return accumulatedChildren; + } + return [ + ...accumulatedChildren, + { + id: child.props.id, + name: child.props.label, + onClick: () => { + const el = document.getElementById(child.props.id); + if (el) { + el.scrollIntoView(); + } + }, + }, + ]; + }, + [] + ); + + const childrenWithProps = Children.map(children, child => + isValidElement(child) + ? cloneElement(child, { + metrics, + onChangeRangeTime, + isLiveStreaming, + stopLiveStreaming, + }) + : null ); if (metrics && subNavItems.length) { diff --git a/x-pack/legacy/plugins/infra/public/pages/metrics/components/sub_section.tsx b/x-pack/legacy/plugins/infra/public/pages/metrics/components/sub_section.tsx index f3db3b1670199..325d510293135 100644 --- a/x-pack/legacy/plugins/infra/public/pages/metrics/components/sub_section.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/metrics/components/sub_section.tsx @@ -23,29 +23,25 @@ export const SubSection: FunctionComponent = ({ isLiveStreaming, stopLiveStreaming, }) => { - if (!children || !metrics) { + const metric = useMemo(() => metrics?.find(m => m.id === id), [id, metrics]); + + if (!children || !metric) { return null; } - const metric = metrics.find(m => m.id === id); - if (!metric) { + + const childrenWithProps = Children.map(children, child => { + if (isValidElement(child)) { + return cloneElement(child, { + metric, + id, + onChangeRangeTime, + isLiveStreaming, + stopLiveStreaming, + }); + } return null; - } - const childrenWithProps = useMemo( - () => - Children.map(children, child => { - if (isValidElement(child)) { - return cloneElement(child, { - metric, - id, - onChangeRangeTime, - isLiveStreaming, - stopLiveStreaming, - }); - } - return null; - }), - [children, metric, id, onChangeRangeTime, isLiveStreaming, stopLiveStreaming] - ); + }); + return (
{label ? ( diff --git a/x-pack/legacy/plugins/infra/public/pages/metrics/containers/with_metrics_time.tsx b/x-pack/legacy/plugins/infra/public/pages/metrics/containers/with_metrics_time.tsx index 432725b6f62b0..64d2ddb67139d 100644 --- a/x-pack/legacy/plugins/infra/public/pages/metrics/containers/with_metrics_time.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/metrics/containers/with_metrics_time.tsx @@ -59,13 +59,10 @@ export const useMetricsTime = () => { const [parsedTimeRange, setParsedTimeRange] = useState(parseRange(defaultRange)); - const updateTimeRange = useCallback( - (range: MetricsTimeInput) => { - setTimeRange(range); - setParsedTimeRange(parseRange(range)); - }, - [setParsedTimeRange] - ); + const updateTimeRange = useCallback((range: MetricsTimeInput) => { + setTimeRange(range); + setParsedTimeRange(parseRange(range)); + }, []); return { timeRange, diff --git a/x-pack/legacy/plugins/infra/public/pages/metrics/index.tsx b/x-pack/legacy/plugins/infra/public/pages/metrics/index.tsx index 93253406aec2d..b330ad02f1022 100644 --- a/x-pack/legacy/plugins/infra/public/pages/metrics/index.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/metrics/index.tsx @@ -112,26 +112,28 @@ export const MetricDetail = withMetricPageProviders( })} /> - + {metadata ? ( + + ) : null} )} diff --git a/x-pack/legacy/plugins/infra/public/utils/cancellable_effect.ts b/x-pack/legacy/plugins/infra/public/utils/cancellable_effect.ts index bb7d253ea1557..a986af07f0c9a 100644 --- a/x-pack/legacy/plugins/infra/public/utils/cancellable_effect.ts +++ b/x-pack/legacy/plugins/infra/public/utils/cancellable_effect.ts @@ -27,5 +27,8 @@ export const useCancellableEffect = ( effect(() => cancellationSignal.isCancelled); return cancellationSignal.cancel; + + // the dependencies are managed externally + // eslint-disable-next-line react-hooks/exhaustive-deps }, deps); }; diff --git a/x-pack/legacy/plugins/infra/public/utils/use_kibana_ui_setting.ts b/x-pack/legacy/plugins/infra/public/utils/use_kibana_ui_setting.ts index c48f95a6521cf..1b08fb4231243 100644 --- a/x-pack/legacy/plugins/infra/public/utils/use_kibana_ui_setting.ts +++ b/x-pack/legacy/plugins/infra/public/utils/use_kibana_ui_setting.ts @@ -28,10 +28,15 @@ import { useObservable } from './use_observable'; export const useKibanaUiSetting = (key: string, defaultValue?: any) => { const uiSettingsClient = npSetup.core.uiSettings; - const uiSetting$ = useMemo(() => uiSettingsClient.get$(key, defaultValue), [uiSettingsClient]); + const uiSetting$ = useMemo(() => uiSettingsClient.get$(key, defaultValue), [ + defaultValue, + key, + uiSettingsClient, + ]); const uiSetting = useObservable(uiSetting$); const setUiSetting = useCallback((value: any) => uiSettingsClient.set(key, value), [ + key, uiSettingsClient, ]); diff --git a/x-pack/legacy/plugins/infra/public/utils/use_tracked_promise.ts b/x-pack/legacy/plugins/infra/public/utils/use_tracked_promise.ts index 366caf0dfb156..c23bab7026aaa 100644 --- a/x-pack/legacy/plugins/infra/public/utils/use_tracked_promise.ts +++ b/x-pack/legacy/plugins/infra/public/utils/use_tracked_promise.ts @@ -190,6 +190,8 @@ export const useTrackedPromise = ( return newPendingPromise.promise; }, + // the dependencies are managed by the caller + // eslint-disable-next-line react-hooks/exhaustive-deps dependencies ); diff --git a/x-pack/legacy/plugins/infra/public/utils/use_url_state.ts b/x-pack/legacy/plugins/infra/public/utils/use_url_state.ts index d03a5aaa9d697..79a5d552bcd78 100644 --- a/x-pack/legacy/plugins/infra/public/utils/use_url_state.ts +++ b/x-pack/legacy/plugins/infra/public/utils/use_url_state.ts @@ -5,10 +5,10 @@ */ import { Location } from 'history'; -import { useMemo, useCallback } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { decode, encode, RisonValue } from 'rison-node'; - import { QueryString } from 'ui/utils/query_string'; + import { useHistory } from './history_context'; export const useUrlState = ({ @@ -16,21 +16,26 @@ export const useUrlState = ({ decodeUrlState, encodeUrlState, urlStateKey, + writeDefaultState = false, }: { defaultState: State; decodeUrlState: (value: RisonValue | undefined) => State | undefined; encodeUrlState: (value: State) => RisonValue | undefined; urlStateKey: string; + writeDefaultState?: boolean; }) => { const history = useHistory(); + // history.location is mutable so we can't reliably use useMemo + const queryString = history?.location ? getQueryStringFromLocation(history.location) : ''; + const urlStateString = useMemo(() => { - if (!history) { + if (!queryString) { return; } - return getParamFromQueryString(getQueryStringFromLocation(history.location), urlStateKey); - }, [history && history.location, urlStateKey]); + return getParamFromQueryString(queryString, urlStateKey); + }, [queryString, urlStateKey]); const decodedState = useMemo(() => decodeUrlState(decodeRisonUrlState(urlStateString)), [ decodeUrlState, @@ -44,27 +49,38 @@ export const useUrlState = ({ const setState = useCallback( (newState: State | undefined) => { - if (!history) { + if (!history || !history.location) { return; } - const location = history.location; + const currentLocation = history.location; const newLocation = replaceQueryStringInLocation( - location, + currentLocation, replaceStateKeyInQueryString( urlStateKey, typeof newState !== 'undefined' ? encodeUrlState(newState) : undefined - )(getQueryStringFromLocation(location)) + )(getQueryStringFromLocation(currentLocation)) ); - if (newLocation !== location) { + if (newLocation !== currentLocation) { history.replace(newLocation); } }, - [encodeUrlState, history, history && history.location, urlStateKey] + [encodeUrlState, history, urlStateKey] ); + const [shouldInitialize, setShouldInitialize] = useState( + writeDefaultState && typeof decodedState === 'undefined' + ); + + useEffect(() => { + if (shouldInitialize) { + setShouldInitialize(false); + setState(defaultState); + } + }, [shouldInitialize, setState, defaultState]); + return [state, setState] as [typeof state, typeof setState]; }; diff --git a/x-pack/legacy/plugins/infra/public/utils/use_visibility_state.ts b/x-pack/legacy/plugins/infra/public/utils/use_visibility_state.ts index 5763834b1cc2a..f4d8b572e4f7f 100644 --- a/x-pack/legacy/plugins/infra/public/utils/use_visibility_state.ts +++ b/x-pack/legacy/plugins/infra/public/utils/use_visibility_state.ts @@ -20,6 +20,6 @@ export const useVisibilityState = (initialState: boolean) => { show, toggle, }), - [isVisible, show, hide] + [hide, isVisible, show, toggle] ); }; From 489b39cfe7cc88dfc2ba77d2f8af93fdf08c36db Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Wed, 11 Dec 2019 14:14:25 +0100 Subject: [PATCH 22/40] Re-enable datemath in from/to canvas timelion args (#52159) --- .../canvas/public/functions/timelion.ts | 31 ++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/x-pack/legacy/plugins/canvas/public/functions/timelion.ts b/x-pack/legacy/plugins/canvas/public/functions/timelion.ts index ee7dd981009d9..4377f2cb4d53b 100644 --- a/x-pack/legacy/plugins/canvas/public/functions/timelion.ts +++ b/x-pack/legacy/plugins/canvas/public/functions/timelion.ts @@ -5,7 +5,10 @@ */ import { flatten } from 'lodash'; +import moment from 'moment-timezone'; import chrome from 'ui/chrome'; +import { npStart } from 'ui/new_platform'; +import { TimeRange } from 'src/plugins/data/common'; import { ExpressionFunction, DatatableRow } from 'src/plugins/expressions/public'; import { fetch } from '../../common/lib/fetch'; // @ts-ignore untyped local @@ -21,6 +24,26 @@ interface Arguments { timezone: string; } +/** + * This function parses a given time range containing date math + * and returns ISO dates. Parsing is done respecting the given time zone. + * @param timeRange time range to parse + * @param timeZone time zone to do the parsing in + */ +function parseDateMath(timeRange: TimeRange, timeZone: string) { + // the datemath plugin always parses dates by using the current default moment time zone. + // to use the configured time zone, we are switching just for the bounds calculation. + const defaultTimezone = moment().zoneName(); + moment.tz.setDefault(timeZone); + + const parsedRange = npStart.plugins.data.query.timefilter.timefilter.calculateBounds(timeRange); + + // reset default moment timezone + moment.tz.setDefault(defaultTimezone); + + return parsedRange; +} + export function timelion(): ExpressionFunction<'timelion', Filter, Arguments, Promise> { const { help, args: argHelp } = getFunctionHelp().timelion; @@ -64,8 +87,8 @@ export function timelion(): ExpressionFunction<'timelion', Filter, Arguments, Pr // workpad, if it exists. Otherwise fall back on the function args. const timeFilter = context.and.find(and => and.type === 'time'); const range = timeFilter - ? { from: timeFilter.from, to: timeFilter.to } - : { from: args.from, to: args.to }; + ? { min: timeFilter.from, max: timeFilter.to } + : parseDateMath({ from: args.from, to: args.to }, args.timezone); const body = { extended: { @@ -79,8 +102,8 @@ export function timelion(): ExpressionFunction<'timelion', Filter, Arguments, Pr }, sheet: [args.query], time: { - from: range.from, - to: range.to, + from: range.min, + to: range.max, interval: args.interval, timezone: args.timezone, }, From b6ea6990c0b679aa890ff87b161f78485f3b7200 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Wed, 11 Dec 2019 14:19:28 +0100 Subject: [PATCH 23/40] Migrate url shortener service (#50896) --- .github/CODEOWNERS | 4 +- .../url_shortening/routes/create_routes.js | 2 - .../server/url_shortening/routes/goto.js | 8 +- .../routes/lib/short_url_lookup.js | 24 ---- .../routes/lib/short_url_lookup.test.js | 37 ------ src/plugins/share/kibana.json | 2 +- .../share/server/index.ts} | 20 +-- src/plugins/share/server/plugin.ts | 37 ++++++ .../share/server/routes/create_routes.ts | 32 +++++ src/plugins/share/server/routes/goto.ts | 64 +++++++++ .../routes/lib/short_url_assert_valid.test.ts | 63 +++++++++ .../routes/lib/short_url_assert_valid.ts | 41 ++++++ .../routes/lib/short_url_lookup.test.ts | 125 ++++++++++++++++++ .../server/routes/lib/short_url_lookup.ts | 84 ++++++++++++ .../share/server/routes/shorten_url.ts | 48 +++++++ .../apis/short_urls/feature_controls.ts | 2 +- 16 files changed, 504 insertions(+), 89 deletions(-) rename src/{legacy/server/url_shortening/routes/shorten_url.js => plugins/share/server/index.ts} (60%) create mode 100644 src/plugins/share/server/plugin.ts create mode 100644 src/plugins/share/server/routes/create_routes.ts create mode 100644 src/plugins/share/server/routes/goto.ts create mode 100644 src/plugins/share/server/routes/lib/short_url_assert_valid.test.ts create mode 100644 src/plugins/share/server/routes/lib/short_url_assert_valid.ts create mode 100644 src/plugins/share/server/routes/lib/short_url_lookup.test.ts create mode 100644 src/plugins/share/server/routes/lib/short_url_lookup.ts create mode 100644 src/plugins/share/server/routes/shorten_url.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index c5e6768c17d46..338fbf2e359b7 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -5,6 +5,8 @@ # App /x-pack/legacy/plugins/lens/ @elastic/kibana-app /x-pack/legacy/plugins/graph/ @elastic/kibana-app +/src/plugins/share/ @elastic/kibana-app +/src/legacy/server/url_shortening/ @elastic/kibana-app /src/legacy/server/sample_data/ @elastic/kibana-app # App Architecture @@ -14,7 +16,6 @@ /src/plugins/kibana_react/ @elastic/kibana-app-arch /src/plugins/kibana_utils/ @elastic/kibana-app-arch /src/plugins/navigation/ @elastic/kibana-app-arch -/src/plugins/share/ @elastic/kibana-app-arch /src/plugins/ui_actions/ @elastic/kibana-app-arch /src/plugins/visualizations/ @elastic/kibana-app-arch /x-pack/plugins/advanced_ui_actions/ @elastic/kibana-app-arch @@ -28,7 +29,6 @@ /src/legacy/core_plugins/kibana/server/routes/api/suggestions/ @elastic/kibana-app-arch /src/legacy/core_plugins/visualizations/ @elastic/kibana-app-arch /src/legacy/server/index_patterns/ @elastic/kibana-app-arch -/src/legacy/server/url_shortening/ @elastic/kibana-app-arch # APM /x-pack/legacy/plugins/apm/ @elastic/apm-ui diff --git a/src/legacy/server/url_shortening/routes/create_routes.js b/src/legacy/server/url_shortening/routes/create_routes.js index 091eabcf47c1f..c6347ace873f7 100644 --- a/src/legacy/server/url_shortening/routes/create_routes.js +++ b/src/legacy/server/url_shortening/routes/create_routes.js @@ -19,12 +19,10 @@ import { shortUrlLookupProvider } from './lib/short_url_lookup'; import { createGotoRoute } from './goto'; -import { createShortenUrlRoute } from './shorten_url'; export function createRoutes(server) { const shortUrlLookup = shortUrlLookupProvider(server); server.route(createGotoRoute({ server, shortUrlLookup })); - server.route(createShortenUrlRoute({ shortUrlLookup })); } diff --git a/src/legacy/server/url_shortening/routes/goto.js b/src/legacy/server/url_shortening/routes/goto.js index 675bc5df50670..60a34499dd2d5 100644 --- a/src/legacy/server/url_shortening/routes/goto.js +++ b/src/legacy/server/url_shortening/routes/goto.js @@ -22,18 +22,12 @@ import { shortUrlAssertValid } from './lib/short_url_assert_valid'; export const createGotoRoute = ({ server, shortUrlLookup }) => ({ method: 'GET', - path: '/goto/{urlId}', + path: '/goto_LP/{urlId}', handler: async function (request, h) { try { const url = await shortUrlLookup.getUrl(request.params.urlId, request); shortUrlAssertValid(url); - const uiSettings = request.getUiSettingsService(); - const stateStoreInSessionStorage = await uiSettings.get('state:storeInSessionStorage'); - if (!stateStoreInSessionStorage) { - return h.redirect(request.getBasePath() + url); - } - const app = server.getHiddenUiAppById('stateSessionStorageRedirect'); return h.renderApp(app, { redirectUrl: url, diff --git a/src/legacy/server/url_shortening/routes/lib/short_url_lookup.js b/src/legacy/server/url_shortening/routes/lib/short_url_lookup.js index c4f6af03d7d93..3a4b96c802c58 100644 --- a/src/legacy/server/url_shortening/routes/lib/short_url_lookup.js +++ b/src/legacy/server/url_shortening/routes/lib/short_url_lookup.js @@ -17,7 +17,6 @@ * under the License. */ -import crypto from 'crypto'; import { get } from 'lodash'; export function shortUrlLookupProvider(server) { @@ -34,29 +33,6 @@ export function shortUrlLookupProvider(server) { } return { - async generateUrlId(url, req) { - const id = crypto.createHash('md5').update(url).digest('hex'); - const savedObjectsClient = req.getSavedObjectsClient(); - const { isConflictError } = savedObjectsClient.errors; - - try { - const doc = await savedObjectsClient.create('url', { - url, - accessCount: 0, - createDate: new Date(), - accessDate: new Date() - }, { id }); - - return doc.id; - } catch (error) { - if (isConflictError(error)) { - return id; - } - - throw error; - } - }, - async getUrl(id, req) { const doc = await req.getSavedObjectsClient().get('url', id); updateMetadata(doc, req); diff --git a/src/legacy/server/url_shortening/routes/lib/short_url_lookup.test.js b/src/legacy/server/url_shortening/routes/lib/short_url_lookup.test.js index 033aeb92926a5..7303682c63e0b 100644 --- a/src/legacy/server/url_shortening/routes/lib/short_url_lookup.test.js +++ b/src/legacy/server/url_shortening/routes/lib/short_url_lookup.test.js @@ -48,43 +48,6 @@ describe('shortUrlLookupProvider', () => { sandbox.restore(); }); - describe('generateUrlId', () => { - it('returns the document id', async () => { - const id = await shortUrl.generateUrlId(URL, req); - expect(id).toEqual(ID); - }); - - it('provides correct arguments to savedObjectsClient', async () => { - await shortUrl.generateUrlId(URL, req); - - sinon.assert.calledOnce(savedObjectsClient.create); - const [type, attributes, options] = savedObjectsClient.create.getCall(0).args; - - expect(type).toEqual(TYPE); - expect(Object.keys(attributes).sort()).toEqual(['accessCount', 'accessDate', 'createDate', 'url']); - expect(attributes.url).toEqual(URL); - expect(options.id).toEqual(ID); - }); - - it('passes persists attributes', async () => { - await shortUrl.generateUrlId(URL, req); - - sinon.assert.calledOnce(savedObjectsClient.create); - const [type, attributes] = savedObjectsClient.create.getCall(0).args; - - expect(type).toEqual(TYPE); - expect(Object.keys(attributes).sort()).toEqual(['accessCount', 'accessDate', 'createDate', 'url']); - expect(attributes.url).toEqual(URL); - }); - - it('gracefully handles version conflict', async () => { - const error = savedObjectsClient.errors.decorateConflictError(new Error()); - savedObjectsClient.create.throws(error); - const id = await shortUrl.generateUrlId(URL, req); - expect(id).toEqual(ID); - }); - }); - describe('getUrl', () => { beforeEach(() => { const attributes = { accessCount: 2, url: URL }; diff --git a/src/plugins/share/kibana.json b/src/plugins/share/kibana.json index bbe393a76c5da..dce2ac9281aba 100644 --- a/src/plugins/share/kibana.json +++ b/src/plugins/share/kibana.json @@ -1,6 +1,6 @@ { "id": "share", "version": "kibana", - "server": false, + "server": true, "ui": true } diff --git a/src/legacy/server/url_shortening/routes/shorten_url.js b/src/plugins/share/server/index.ts similarity index 60% rename from src/legacy/server/url_shortening/routes/shorten_url.js rename to src/plugins/share/server/index.ts index 0203e9373384a..9e574314f8000 100644 --- a/src/legacy/server/url_shortening/routes/shorten_url.js +++ b/src/plugins/share/server/index.ts @@ -17,19 +17,9 @@ * under the License. */ -import { handleShortUrlError } from './lib/short_url_error'; -import { shortUrlAssertValid } from './lib/short_url_assert_valid'; +import { PluginInitializerContext } from '../../../core/server'; +import { SharePlugin } from './plugin'; -export const createShortenUrlRoute = ({ shortUrlLookup }) => ({ - method: 'POST', - path: '/api/shorten_url', - handler: async function (request) { - try { - shortUrlAssertValid(request.payload.url); - const urlId = await shortUrlLookup.generateUrlId(request.payload.url, request); - return { urlId }; - } catch (err) { - throw handleShortUrlError(err); - } - } -}); +export function plugin(initializerContext: PluginInitializerContext) { + return new SharePlugin(initializerContext); +} diff --git a/src/plugins/share/server/plugin.ts b/src/plugins/share/server/plugin.ts new file mode 100644 index 0000000000000..bcb681a50652a --- /dev/null +++ b/src/plugins/share/server/plugin.ts @@ -0,0 +1,37 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { CoreSetup, Plugin, PluginInitializerContext } from 'kibana/server'; +import { createRoutes } from './routes/create_routes'; + +export class SharePlugin implements Plugin { + constructor(private readonly initializerContext: PluginInitializerContext) {} + + public async setup(core: CoreSetup) { + createRoutes(core, this.initializerContext.logger.get()); + } + + public start() { + this.initializerContext.logger.get().debug('Starting plugin'); + } + + public stop() { + this.initializerContext.logger.get().debug('Stopping plugin'); + } +} diff --git a/src/plugins/share/server/routes/create_routes.ts b/src/plugins/share/server/routes/create_routes.ts new file mode 100644 index 0000000000000..bd4b6fdb08791 --- /dev/null +++ b/src/plugins/share/server/routes/create_routes.ts @@ -0,0 +1,32 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { CoreSetup, Logger } from 'kibana/server'; + +import { shortUrlLookupProvider } from './lib/short_url_lookup'; +import { createGotoRoute } from './goto'; +import { createShortenUrlRoute } from './shorten_url'; + +export function createRoutes({ http }: CoreSetup, logger: Logger) { + const shortUrlLookup = shortUrlLookupProvider({ logger }); + const router = http.createRouter(); + + createGotoRoute({ router, shortUrlLookup, http }); + createShortenUrlRoute({ router, shortUrlLookup }); +} diff --git a/src/plugins/share/server/routes/goto.ts b/src/plugins/share/server/routes/goto.ts new file mode 100644 index 0000000000000..7343dc1bd34a2 --- /dev/null +++ b/src/plugins/share/server/routes/goto.ts @@ -0,0 +1,64 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { CoreSetup, IRouter } from 'kibana/server'; +import { schema } from '@kbn/config-schema'; + +import { shortUrlAssertValid } from './lib/short_url_assert_valid'; +import { ShortUrlLookupService } from './lib/short_url_lookup'; + +export const createGotoRoute = ({ + router, + shortUrlLookup, + http, +}: { + router: IRouter; + shortUrlLookup: ShortUrlLookupService; + http: CoreSetup['http']; +}) => { + router.get( + { + path: '/goto/{urlId}', + validate: { + params: schema.object({ urlId: schema.string() }), + }, + }, + router.handleLegacyErrors(async function(context, request, response) { + const url = await shortUrlLookup.getUrl(request.params.urlId, { + savedObjects: context.core.savedObjects.client, + }); + shortUrlAssertValid(url); + + const uiSettings = context.core.uiSettings.client; + const stateStoreInSessionStorage = await uiSettings.get('state:storeInSessionStorage'); + if (!stateStoreInSessionStorage) { + return response.redirected({ + headers: { + location: http.basePath.prepend(url), + }, + }); + } + return response.redirected({ + headers: { + location: http.basePath.prepend('/goto_LP/' + request.params.urlId), + }, + }); + }) + ); +}; diff --git a/src/plugins/share/server/routes/lib/short_url_assert_valid.test.ts b/src/plugins/share/server/routes/lib/short_url_assert_valid.test.ts new file mode 100644 index 0000000000000..f83073e6aefe9 --- /dev/null +++ b/src/plugins/share/server/routes/lib/short_url_assert_valid.test.ts @@ -0,0 +1,63 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { shortUrlAssertValid } from './short_url_assert_valid'; + +describe('shortUrlAssertValid()', () => { + const invalid = [ + ['protocol', 'http://localhost:5601/app/kibana'], + ['protocol', 'https://localhost:5601/app/kibana'], + ['protocol', 'mailto:foo@bar.net'], + ['protocol', 'javascript:alert("hi")'], // eslint-disable-line no-script-url + ['hostname', 'localhost/app/kibana'], + ['hostname and port', 'local.host:5601/app/kibana'], + ['hostname and auth', 'user:pass@localhost.net/app/kibana'], + ['path traversal', '/app/../../not-kibana'], + ['deep path', '/app/kibana/foo'], + ['deep path', '/app/kibana/foo/bar'], + ['base path', '/base/app/kibana'], + ]; + + invalid.forEach(([desc, url]) => { + it(`fails when url has ${desc}`, () => { + try { + shortUrlAssertValid(url); + throw new Error(`expected assertion to throw`); + } catch (err) { + if (!err || !err.isBoom) { + throw err; + } + } + }); + }); + + const valid = [ + '/app/kibana', + '/app/monitoring#angular/route', + '/app/text#document-id', + '/app/some?with=query', + '/app/some?with=query#and-a-hash', + ]; + + valid.forEach(url => { + it(`allows ${url}`, () => { + shortUrlAssertValid(url); + }); + }); +}); diff --git a/src/plugins/share/server/routes/lib/short_url_assert_valid.ts b/src/plugins/share/server/routes/lib/short_url_assert_valid.ts new file mode 100644 index 0000000000000..2f120bbc03cd7 --- /dev/null +++ b/src/plugins/share/server/routes/lib/short_url_assert_valid.ts @@ -0,0 +1,41 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { parse } from 'url'; +import { trim } from 'lodash'; +import Boom from 'boom'; + +export function shortUrlAssertValid(url: string) { + const { protocol, hostname, pathname } = parse(url); + + if (protocol) { + throw Boom.notAcceptable(`Short url targets cannot have a protocol, found "${protocol}"`); + } + + if (hostname) { + throw Boom.notAcceptable(`Short url targets cannot have a hostname, found "${hostname}"`); + } + + const pathnameParts = trim(pathname, '/').split('/'); + if (pathnameParts.length !== 2) { + throw Boom.notAcceptable( + `Short url target path must be in the format "/app/{{appId}}", found "${pathname}"` + ); + } +} diff --git a/src/plugins/share/server/routes/lib/short_url_lookup.test.ts b/src/plugins/share/server/routes/lib/short_url_lookup.test.ts new file mode 100644 index 0000000000000..87e2b7b726e59 --- /dev/null +++ b/src/plugins/share/server/routes/lib/short_url_lookup.test.ts @@ -0,0 +1,125 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { shortUrlLookupProvider, ShortUrlLookupService } from './short_url_lookup'; +import { SavedObjectsClientContract, Logger } from 'kibana/server'; +import { SavedObjectsClient } from '../../../../../core/server'; + +describe('shortUrlLookupProvider', () => { + const ID = 'bf00ad16941fc51420f91a93428b27a0'; + const TYPE = 'url'; + const URL = 'http://elastic.co'; + + let savedObjects: jest.Mocked; + let deps: { savedObjects: SavedObjectsClientContract }; + let shortUrl: ShortUrlLookupService; + + beforeEach(() => { + savedObjects = ({ + get: jest.fn(), + create: jest.fn(() => Promise.resolve({ id: ID })), + update: jest.fn(), + errors: SavedObjectsClient.errors, + } as unknown) as jest.Mocked; + + deps = { savedObjects }; + shortUrl = shortUrlLookupProvider({ logger: ({ warn: () => {} } as unknown) as Logger }); + }); + + describe('generateUrlId', () => { + it('returns the document id', async () => { + const id = await shortUrl.generateUrlId(URL, deps); + expect(id).toEqual(ID); + }); + + it('provides correct arguments to savedObjectsClient', async () => { + await shortUrl.generateUrlId(URL, { savedObjects }); + + expect(savedObjects.create).toHaveBeenCalledTimes(1); + const [type, attributes, options] = savedObjects.create.mock.calls[0]; + + expect(type).toEqual(TYPE); + expect(Object.keys(attributes).sort()).toEqual([ + 'accessCount', + 'accessDate', + 'createDate', + 'url', + ]); + expect(attributes.url).toEqual(URL); + expect(options!.id).toEqual(ID); + }); + + it('passes persists attributes', async () => { + await shortUrl.generateUrlId(URL, deps); + + expect(savedObjects.create).toHaveBeenCalledTimes(1); + const [type, attributes] = savedObjects.create.mock.calls[0]; + + expect(type).toEqual(TYPE); + expect(Object.keys(attributes).sort()).toEqual([ + 'accessCount', + 'accessDate', + 'createDate', + 'url', + ]); + expect(attributes.url).toEqual(URL); + }); + + it('gracefully handles version conflict', async () => { + const error = savedObjects.errors.decorateConflictError(new Error()); + savedObjects.create.mockImplementation(() => { + throw error; + }); + const id = await shortUrl.generateUrlId(URL, deps); + expect(id).toEqual(ID); + }); + }); + + describe('getUrl', () => { + beforeEach(() => { + const attributes = { accessCount: 2, url: URL }; + savedObjects.get.mockResolvedValue({ id: ID, attributes, type: 'url', references: [] }); + }); + + it('provides the ID to savedObjectsClient', async () => { + await shortUrl.getUrl(ID, { savedObjects }); + + expect(savedObjects.get).toHaveBeenCalledTimes(1); + expect(savedObjects.get).toHaveBeenCalledWith(TYPE, ID); + }); + + it('returns the url', async () => { + const response = await shortUrl.getUrl(ID, deps); + expect(response).toEqual(URL); + }); + + it('increments accessCount', async () => { + await shortUrl.getUrl(ID, { savedObjects }); + + expect(savedObjects.update).toHaveBeenCalledTimes(1); + + const [type, id, attributes] = savedObjects.update.mock.calls[0]; + + expect(type).toEqual(TYPE); + expect(id).toEqual(ID); + expect(Object.keys(attributes).sort()).toEqual(['accessCount', 'accessDate']); + expect(attributes.accessCount).toEqual(3); + }); + }); +}); diff --git a/src/plugins/share/server/routes/lib/short_url_lookup.ts b/src/plugins/share/server/routes/lib/short_url_lookup.ts new file mode 100644 index 0000000000000..0d8a9c86621de --- /dev/null +++ b/src/plugins/share/server/routes/lib/short_url_lookup.ts @@ -0,0 +1,84 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import crypto from 'crypto'; +import { get } from 'lodash'; + +import { Logger, SavedObject, SavedObjectsClientContract } from 'kibana/server'; + +export interface ShortUrlLookupService { + generateUrlId(url: string, deps: { savedObjects: SavedObjectsClientContract }): Promise; + getUrl(url: string, deps: { savedObjects: SavedObjectsClientContract }): Promise; +} + +export function shortUrlLookupProvider({ logger }: { logger: Logger }): ShortUrlLookupService { + async function updateMetadata( + doc: SavedObject, + { savedObjects }: { savedObjects: SavedObjectsClientContract } + ) { + try { + await savedObjects.update('url', doc.id, { + accessDate: new Date().valueOf(), + accessCount: get(doc, 'attributes.accessCount', 0) + 1, + }); + } catch (error) { + logger.warn('Warning: Error updating url metadata'); + logger.warn(error); + // swallow errors. It isn't critical if there is no update. + } + } + + return { + async generateUrlId(url, { savedObjects }) { + const id = crypto + .createHash('md5') + .update(url) + .digest('hex'); + const { isConflictError } = savedObjects.errors; + + try { + const doc = await savedObjects.create( + 'url', + { + url, + accessCount: 0, + createDate: new Date().valueOf(), + accessDate: new Date().valueOf(), + }, + { id } + ); + + return doc.id; + } catch (error) { + if (isConflictError(error)) { + return id; + } + + throw error; + } + }, + + async getUrl(id, { savedObjects }) { + const doc = await savedObjects.get('url', id); + updateMetadata(doc, { savedObjects }); + + return doc.attributes.url; + }, + }; +} diff --git a/src/plugins/share/server/routes/shorten_url.ts b/src/plugins/share/server/routes/shorten_url.ts new file mode 100644 index 0000000000000..116b90c6971c5 --- /dev/null +++ b/src/plugins/share/server/routes/shorten_url.ts @@ -0,0 +1,48 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { IRouter } from 'kibana/server'; +import { schema } from '@kbn/config-schema'; + +import { shortUrlAssertValid } from './lib/short_url_assert_valid'; +import { ShortUrlLookupService } from './lib/short_url_lookup'; + +export const createShortenUrlRoute = ({ + shortUrlLookup, + router, +}: { + shortUrlLookup: ShortUrlLookupService; + router: IRouter; +}) => { + router.post( + { + path: '/api/shorten_url', + validate: { + body: schema.object({ url: schema.string() }), + }, + }, + router.handleLegacyErrors(async function(context, request, response) { + shortUrlAssertValid(request.body.url); + const urlId = await shortUrlLookup.generateUrlId(request.body.url, { + savedObjects: context.core.savedObjects.client, + }); + return response.ok({ body: { urlId } }); + }) + ); +}; diff --git a/x-pack/test/api_integration/apis/short_urls/feature_controls.ts b/x-pack/test/api_integration/apis/short_urls/feature_controls.ts index 06fd971399ea3..db5e11ef367ad 100644 --- a/x-pack/test/api_integration/apis/short_urls/feature_controls.ts +++ b/x-pack/test/api_integration/apis/short_urls/feature_controls.ts @@ -107,7 +107,7 @@ export default function featureControlsTests({ getService }: FtrProviderContext) expect(resp.status).to.eql(302); expect(resp.headers.location).to.eql('/app/kibana#foo/bar/baz'); } else { - expect(resp.status).to.eql(500); + expect(resp.status).to.eql(403); expect(resp.headers.location).to.eql(undefined); } }); From 3130759c47ca108a2e86c61007dcd2554cbc66c2 Mon Sep 17 00:00:00 2001 From: Robert Oskamp Date: Wed, 11 Dec 2019 16:25:48 +0100 Subject: [PATCH 24/40] [ML] Functional tests - export service types (#52612) With this PR the types of the ML services that are used in other services are exported from the service file to allow a cleaner re-use. --- x-pack/test/functional/services/machine_learning/api.ts | 3 +++ .../test/functional/services/machine_learning/common.ts | 3 +++ .../functional/services/machine_learning/custom_urls.ts | 3 +++ .../services/machine_learning/data_frame_analytics.ts | 5 ++--- .../services/machine_learning/job_management.ts | 5 ++--- .../services/machine_learning/job_wizard_advanced.ts | 5 ++--- .../services/machine_learning/job_wizard_common.ts | 9 ++++----- 7 files changed, 19 insertions(+), 14 deletions(-) diff --git a/x-pack/test/functional/services/machine_learning/api.ts b/x-pack/test/functional/services/machine_learning/api.ts index 1995f37782948..1f3711ff5e506 100644 --- a/x-pack/test/functional/services/machine_learning/api.ts +++ b/x-pack/test/functional/services/machine_learning/api.ts @@ -4,12 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ import expect from '@kbn/expect'; +import { ProvidedType } from '@kbn/test/types/ftr'; import { FtrProviderContext } from '../../ftr_provider_context'; import { JOB_STATE, DATAFEED_STATE } from '../../../../legacy/plugins/ml/common/constants/states'; import { DATA_FRAME_TASK_STATE } from '../../../../legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common'; +export type MlApi = ProvidedType; + export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { const es = getService('legacyEs'); const log = getService('log'); diff --git a/x-pack/test/functional/services/machine_learning/common.ts b/x-pack/test/functional/services/machine_learning/common.ts index 12b9e8a1cfb29..35ee32fa5d94e 100644 --- a/x-pack/test/functional/services/machine_learning/common.ts +++ b/x-pack/test/functional/services/machine_learning/common.ts @@ -3,6 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { ProvidedType } from '@kbn/test/types/ftr'; import { FtrProviderContext } from '../../ftr_provider_context'; @@ -11,6 +12,8 @@ interface SetValueOptions { typeCharByChar?: boolean; } +export type MlCommon = ProvidedType; + export function MachineLearningCommonProvider({ getService }: FtrProviderContext) { const log = getService('log'); const retry = getService('retry'); diff --git a/x-pack/test/functional/services/machine_learning/custom_urls.ts b/x-pack/test/functional/services/machine_learning/custom_urls.ts index dc6e4a2fccb10..6842908462018 100644 --- a/x-pack/test/functional/services/machine_learning/custom_urls.ts +++ b/x-pack/test/functional/services/machine_learning/custom_urls.ts @@ -4,9 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ import expect from '@kbn/expect'; +import { ProvidedType } from '@kbn/test/types/ftr'; import { FtrProviderContext } from '../../ftr_provider_context'; +export type MlCustomUrls = ProvidedType; + export function MachineLearningCustomUrlsProvider({ getService }: FtrProviderContext) { const testSubjects = getService('testSubjects'); diff --git a/x-pack/test/functional/services/machine_learning/data_frame_analytics.ts b/x-pack/test/functional/services/machine_learning/data_frame_analytics.ts index 163c3c60ffdab..8c8b5db1d2c52 100644 --- a/x-pack/test/functional/services/machine_learning/data_frame_analytics.ts +++ b/x-pack/test/functional/services/machine_learning/data_frame_analytics.ts @@ -3,16 +3,15 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { ProvidedType } from '@kbn/test/types/ftr'; import { FtrProviderContext } from '../../ftr_provider_context'; -import { MachineLearningAPIProvider } from './api'; +import { MlApi } from './api'; import { DATA_FRAME_TASK_STATE } from '../../../../legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common'; export function MachineLearningDataFrameAnalyticsProvider( { getService }: FtrProviderContext, - mlApi: ProvidedType + mlApi: MlApi ) { const testSubjects = getService('testSubjects'); diff --git a/x-pack/test/functional/services/machine_learning/job_management.ts b/x-pack/test/functional/services/machine_learning/job_management.ts index 5ffb235a828d6..1fa1f62a9ae11 100644 --- a/x-pack/test/functional/services/machine_learning/job_management.ts +++ b/x-pack/test/functional/services/machine_learning/job_management.ts @@ -3,16 +3,15 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { ProvidedType } from '@kbn/test/types/ftr'; import { FtrProviderContext } from '../../ftr_provider_context'; -import { MachineLearningAPIProvider } from './api'; +import { MlApi } from './api'; import { JOB_STATE, DATAFEED_STATE } from '../../../../legacy/plugins/ml/common/constants/states'; export function MachineLearningJobManagementProvider( { getService }: FtrProviderContext, - mlApi: ProvidedType + mlApi: MlApi ) { const testSubjects = getService('testSubjects'); diff --git a/x-pack/test/functional/services/machine_learning/job_wizard_advanced.ts b/x-pack/test/functional/services/machine_learning/job_wizard_advanced.ts index ab53b0412ca35..755091ca10f3b 100644 --- a/x-pack/test/functional/services/machine_learning/job_wizard_advanced.ts +++ b/x-pack/test/functional/services/machine_learning/job_wizard_advanced.ts @@ -4,14 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ import expect from '@kbn/expect'; -import { ProvidedType } from '@kbn/test/types/ftr'; import { FtrProviderContext } from '../../ftr_provider_context'; -import { MachineLearningCommonProvider } from './common'; +import { MlCommon } from './common'; export function MachineLearningJobWizardAdvancedProvider( { getService }: FtrProviderContext, - mlCommon: ProvidedType + mlCommon: MlCommon ) { const comboBox = getService('comboBox'); const testSubjects = getService('testSubjects'); diff --git a/x-pack/test/functional/services/machine_learning/job_wizard_common.ts b/x-pack/test/functional/services/machine_learning/job_wizard_common.ts index b9e6822c8f41a..c2f408276d9e4 100644 --- a/x-pack/test/functional/services/machine_learning/job_wizard_common.ts +++ b/x-pack/test/functional/services/machine_learning/job_wizard_common.ts @@ -4,16 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ import expect from '@kbn/expect'; -import { ProvidedType } from '@kbn/test/types/ftr'; import { FtrProviderContext } from '../../ftr_provider_context'; -import { MachineLearningCommonProvider } from './common'; -import { MachineLearningCustomUrlsProvider } from './custom_urls'; +import { MlCommon } from './common'; +import { MlCustomUrls } from './custom_urls'; export function MachineLearningJobWizardCommonProvider( { getService }: FtrProviderContext, - mlCommon: ProvidedType, - customUrls: ProvidedType + mlCommon: MlCommon, + customUrls: MlCustomUrls ) { const comboBox = getService('comboBox'); const retry = getService('retry'); From 8c19b5e017dd1d2d3adaca641937e361a16c9294 Mon Sep 17 00:00:00 2001 From: Chris Roberson Date: Wed, 11 Dec 2019 10:35:34 -0500 Subject: [PATCH 25/40] Skip all logstash pipeline tests (#52743) --- .../apis/monitoring/logstash/multicluster_pipelines.js | 2 +- .../test/api_integration/apis/monitoring/logstash/pipelines.js | 2 +- x-pack/test/functional/apps/monitoring/logstash/pipelines.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/test/api_integration/apis/monitoring/logstash/multicluster_pipelines.js b/x-pack/test/api_integration/apis/monitoring/logstash/multicluster_pipelines.js index bea09562bdb11..fa26a0dcac794 100644 --- a/x-pack/test/api_integration/apis/monitoring/logstash/multicluster_pipelines.js +++ b/x-pack/test/api_integration/apis/monitoring/logstash/multicluster_pipelines.js @@ -11,7 +11,7 @@ export default function ({ getService }) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); - describe('pipelines listing multicluster', () => { + describe.skip('pipelines listing multicluster', () => { const archive = 'monitoring/logstash_pipelines_multicluster'; const timeRange = { min: '2019-11-11T15:13:45.266Z', diff --git a/x-pack/test/api_integration/apis/monitoring/logstash/pipelines.js b/x-pack/test/api_integration/apis/monitoring/logstash/pipelines.js index 0852b8293886e..9e2160a69c726 100644 --- a/x-pack/test/api_integration/apis/monitoring/logstash/pipelines.js +++ b/x-pack/test/api_integration/apis/monitoring/logstash/pipelines.js @@ -11,7 +11,7 @@ export default function ({ getService }) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); - describe('pipelines', () => { + describe.skip('pipelines', () => { const archive = 'monitoring/logstash/changing_pipelines'; const timeRange = { min: '2019-11-04T15:40:44.855Z', diff --git a/x-pack/test/functional/apps/monitoring/logstash/pipelines.js b/x-pack/test/functional/apps/monitoring/logstash/pipelines.js index f4d2a5a4a20a5..3aacd9e66dd4a 100644 --- a/x-pack/test/functional/apps/monitoring/logstash/pipelines.js +++ b/x-pack/test/functional/apps/monitoring/logstash/pipelines.js @@ -14,7 +14,7 @@ export default function ({ getService, getPageObjects }) { const pipelinesList = getService('monitoringLogstashPipelines'); const lsClusterSummaryStatus = getService('monitoringLogstashSummaryStatus'); - describe('Logstash pipelines', () => { + describe.skip('Logstash pipelines', () => { const { setup, tearDown } = getLifecycleMethods(getService, getPageObjects); before(async () => { From 6f79046ff2ef21978277d4775eda296c8adb549b Mon Sep 17 00:00:00 2001 From: Ben Skelker <54019610+benskelker@users.noreply.github.com> Date: Wed, 11 Dec 2019 17:41:24 +0200 Subject: [PATCH 26/40] [SIEM] Improves map configuration text on Network page (#52469) * updates SIEM network page maps conf message * corrects link atts * updated message * updated message again * finally * updates after feedback --- .../index_patterns_missing_prompt.test.tsx.snap | 11 +++++++++-- .../embeddables/index_patterns_missing_prompt.tsx | 13 +++++++++++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/__snapshots__/index_patterns_missing_prompt.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/embeddables/__snapshots__/index_patterns_missing_prompt.test.tsx.snap index fb896059460b9..6794aab205703 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/__snapshots__/index_patterns_missing_prompt.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/__snapshots__/index_patterns_missing_prompt.test.tsx.snap @@ -16,7 +16,7 @@ exports[`IndexPatternsMissingPrompt renders correctly against snapshot 1`] = `

beats , + "defaultIndex": + siem:defaultIndex + , "example": ./packetbeat setup , @@ -39,7 +46,7 @@ exports[`IndexPatternsMissingPrompt renders correctly against snapshot 1`] = `

diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/index_patterns_missing_prompt.tsx b/x-pack/legacy/plugins/siem/public/components/embeddables/index_patterns_missing_prompt.tsx index 1e29676415d79..6533be49c3430 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/index_patterns_missing_prompt.tsx +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/index_patterns_missing_prompt.tsx @@ -21,9 +21,18 @@ export const IndexPatternsMissingPromptComponent = () => ( <>

+ {'siem:defaultIndex'} + + ), beats: ( (

From c962009df6c73e9335cfcb7889729a989638b6c0 Mon Sep 17 00:00:00 2001 From: Pete Harverson Date: Wed, 11 Dec 2019 15:54:05 +0000 Subject: [PATCH 27/40] [ML] Adds Enterprise license to Start trial text on data viz page (#52749) --- .../datavisualizer/datavisualizer_selector.tsx | 8 ++++---- x-pack/plugins/translations/translations/ja-JP.json | 8 +++----- x-pack/plugins/translations/translations/zh-CN.json | 10 ++++------ 3 files changed, 11 insertions(+), 15 deletions(-) diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx index e6f6d4581c706..c24061cb052b8 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx @@ -32,13 +32,13 @@ function startTrialDescription() { ), diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 55147c7863d1f..054382ed0fa81 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -463,9 +463,6 @@ "common.ui.management.breadcrumb": "管理", "common.ui.management.connectDataDisplayName": "データに接続", "common.ui.management.displayName": "管理", - "management.editIndexPattern.createIndex.defaultButtonDescription": "すべてのデータに完全集約を実行", - "management.editIndexPattern.createIndex.defaultButtonText": "標準インデックスパターン", - "management.editIndexPattern.createIndex.defaultTypeName": "インデックスパターン", "common.ui.management.nav.menu": "管理メニュー", "common.ui.modals.cancelButtonLabel": "キャンセル", "common.ui.notify.fatalError.errorStatusMessage": "エラー {errStatus} {errStatusText}: {errMessage}", @@ -566,6 +563,9 @@ "common.ui.aggTypes.scaleMetricsLabel": "メトリック値のスケーリング (廃止)", "common.ui.aggTypes.scaleMetricsTooltip": "これを有効にすると、手動最低間隔を選択し、広い間隔が使用された場合、カウントと合計メトリックが手動で選択された間隔にスケーリングされます。", "common.ui.aggTypes.buckets.ranges.rangesFormatMessage": "{gte} {from} と {lt} {to}", + "management.editIndexPattern.createIndex.defaultButtonDescription": "すべてのデータに完全集約を実行", + "management.editIndexPattern.createIndex.defaultButtonText": "標準インデックスパターン", + "management.editIndexPattern.createIndex.defaultTypeName": "インデックスパターン", "core.ui.overlays.banner.attentionTitle": "注意", "core.ui.overlays.banner.closeButtonLabel": "閉じる", "core.ui.chrome.headerGlobalNav.goHomePageIconAriaLabel": "ホームページに移動", @@ -6900,8 +6900,6 @@ "xpack.ml.datavisualizer.selector.startTrialButtonLabel": "トライアルを開始", "xpack.ml.datavisualizer.selector.startTrialTitle": "トライアルを開始", "xpack.ml.datavisualizer.selector.uploadFileButtonLabel": "ファイルをアップロード", - "xpack.ml.datavisualizer.startTrial.fullMLFeaturesDescription": "{platinumSubscriptionLink} が提供するすべての機械学習機能を体験するには、30 日間のトライアルを開始してください。", - "xpack.ml.datavisualizer.startTrial.platinumSubscriptionTitle": "プラチナサブスクリプション", "xpack.ml.dataVisualizerPageLabel": "データビジュアライザー", "xpack.ml.explorer.annotationsTitle": "注釈", "xpack.ml.explorer.anomaliesTitle": "異常", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 974467a8d20d0..1977da8ac9100 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -463,9 +463,6 @@ "common.ui.management.breadcrumb": "管理", "common.ui.management.connectDataDisplayName": "连接数据", "common.ui.management.displayName": "管理", - "management.editIndexPattern.createIndex.defaultButtonDescription": "对任何数据执行完全聚合", - "management.editIndexPattern.createIndex.defaultButtonText": "标准索引模式", - "management.editIndexPattern.createIndex.defaultTypeName": "索引模式", "common.ui.management.nav.menu": "管理菜单", "common.ui.modals.cancelButtonLabel": "取消", "common.ui.notify.fatalError.errorStatusMessage": "错误 {errStatus} {errStatusText}:{errMessage}", @@ -567,6 +564,9 @@ "common.ui.aggTypes.scaleMetricsLabel": "缩放指标值(已弃用)", "common.ui.aggTypes.scaleMetricsTooltip": "如果选择手动最小时间间隔并将使用较大的时间间隔,则启用此设置将使计数和求和指标缩放到手动选择的时间间隔。", "common.ui.aggTypes.buckets.ranges.rangesFormatMessage": "{gte} {from} 且 {lt} {to}", + "management.editIndexPattern.createIndex.defaultButtonDescription": "对任何数据执行完全聚合", + "management.editIndexPattern.createIndex.defaultButtonText": "标准索引模式", + "management.editIndexPattern.createIndex.defaultTypeName": "索引模式", "core.ui.overlays.banner.attentionTitle": "注意", "core.ui.overlays.banner.closeButtonLabel": "关闭", "core.ui.chrome.headerGlobalNav.goHomePageIconAriaLabel": "前往主页", @@ -6902,8 +6902,6 @@ "xpack.ml.datavisualizer.selector.startTrialButtonLabel": "开始试用", "xpack.ml.datavisualizer.selector.startTrialTitle": "开始试用", "xpack.ml.datavisualizer.selector.uploadFileButtonLabel": "上传文件", - "xpack.ml.datavisualizer.startTrial.fullMLFeaturesDescription": "要体验 {platinumSubscriptionLink} 提供的完整 Machine Learning 功能,可以安装为期 30 天的试用版。", - "xpack.ml.datavisualizer.startTrial.platinumSubscriptionTitle": "白金级订阅", "xpack.ml.dataVisualizerPageLabel": "数据可视化工具", "xpack.ml.explorer.annotationsTitle": "注释", "xpack.ml.explorer.anomaliesTitle": "异常", @@ -12825,4 +12823,4 @@ "xpack.licensing.welcomeBanner.licenseIsExpiredDescription.updateYourLicenseLinkText": "更新您的许可", "xpack.licensing.welcomeBanner.licenseIsExpiredTitle": "您的{licenseType}许可已过期" } -} +} \ No newline at end of file From 16447626c964ece49ebbdc90768b5c7e1b1f4a39 Mon Sep 17 00:00:00 2001 From: Victor Martinez Date: Wed, 11 Dec 2019 15:56:41 +0000 Subject: [PATCH 28/40] ci(jenkins): simplify the kibana setup for the e2e tests (#52729) --- .../plugins/apm/cypress/ci/kibana.dev.yml | 53 ------------------- 1 file changed, 53 deletions(-) diff --git a/x-pack/legacy/plugins/apm/cypress/ci/kibana.dev.yml b/x-pack/legacy/plugins/apm/cypress/ci/kibana.dev.yml index 900fb8d47cecf..3082391f23a15 100644 --- a/x-pack/legacy/plugins/apm/cypress/ci/kibana.dev.yml +++ b/x-pack/legacy/plugins/apm/cypress/ci/kibana.dev.yml @@ -1,57 +1,4 @@ ## # Disabled plugins ######################## -# data.enabled: false -# interpreter.enabled: false -# visualizations.enabled: false -# xpack.apm.enabled: false -# console.enabled: false -console_extensions.enabled: false -dashboard_embeddable_container.enabled: false -dashboard_mode.enabled: false -embeddable_api.enabled: false -file_upload.enabled: false -# input_control_vis.enabled: false -inspector_views.enabled: false -kibana_react.enabled: false -markdown_vis.enabled: false -metric_vis.enabled: false -metrics.enabled: false -region_map.enabled: false -table_vis.enabled: false -tagcloud.enabled: false -tile_map.enabled: false -timelion.enabled: false -ui_metric.enabled: false -vega.enabled: false -xpack.actions.enabled: false -xpack.alerting.enabled: false -xpack.beats.enabled: false -xpack.canvas.enabled: false -xpack.cloud.enabled: false -xpack.code.enabled: false -xpack.encryptedSavedObjects.enabled: false -xpack.graph.enabled: false -xpack.grokdebugger.enabled: false -xpack.index_management.enabled: false -xpack.infra.enabled: false -# xpack.license_management.enabled: false -xpack.lens.enabled: false -xpack.logstash.enabled: false -xpack.maps.enabled: false -xpack.ml.enabled: false -xpack.monitoring.enabled: false -xpack.oss_telemetry.enabled: false -xpack.remote_clusters.enabled: false -xpack.rollup.enabled: false -xpack.searchprofiler.enabled: false -# xpack.security.enabled: false -xpack.siem.enabled: false -xpack.snapshot_restore.enabled: false -xpack.spaces.enabled: false -xpack.task_manager.enabled: false -xpack.tilemap.enabled: false -xpack.upgrade_assistant.enabled: false -xpack.uptime.enabled: false -xpack.watcher.enabled: false logging.verbose: true From 4f2a6f8362a810719e091a25008e44f958aaac06 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Wed, 11 Dec 2019 15:58:11 +0000 Subject: [PATCH 29/40] [ML] Replacing angular routing (#51842) * [ML] Replacing angular routing * removing old files * changing overview * renaming overview route * adding df analytics routes * adding timeseriesexplorer route * removing old files * adding route for explorer * adding access denied page * adding module view or create redirect * fixing job cloning * adding breadcrumb system * removing old breadcrumbs files * fix include * enabling management section * injecting app dependencies * fixing missed dependencies * fixing saved searches * fixing type errors * removing included data start * code clean up * updating translations * fixing router test failures * fixing functional tests * removing last use of SavedSearch * removing comment * fixing bug in line chart query * improving saved search jobs * fixing data viz functional test * adding comment * dealing with time range error * removing unnecessary chrome imports * cleaning up code * moving resolver to own file * changes based on review * fixing index data viz on basic license * fixing edit calendar * adding create job breadcrumb * fixing results appstate * fixing management links * updating new job constants file * fixing rebase conflicts * removing commented out code * adding additional text to the resolver error --- .../legacy/plugins/ml/common/constants/app.ts | 7 + .../ml/common/constants/feature_flags.ts | 3 +- .../constants/new_job.ts} | 0 .../legacy/plugins/ml/common/types/kibana.ts | 11 ++ .../plugins/ml/common/util/job_utils.js | 2 +- x-pack/legacy/plugins/ml/index.ts | 4 +- x-pack/legacy/plugins/ml/kibana.json | 8 + .../application/access_denied/index.tsx | 34 +--- .../public/application/access_denied/page.tsx | 2 +- .../plugins/ml/public/application/app.js | 36 ---- .../plugins/ml/public/application/app.tsx | 53 ++++++ .../annotations_table/annotations_table.js | 3 +- .../components/anomalies_table/links_menu.js | 2 +- .../data_recognizer/data_recognizer.d.ts | 4 +- .../data_recognizer/recognized_result.js | 2 +- .../components/navigation_menu/main_tabs.tsx | 3 +- .../components/navigation_menu/tabs.tsx | 3 +- .../contexts/kibana/__mocks__/saved_search.ts | 27 ++- .../contexts/kibana/kibana_context.ts | 7 +- .../data_frame_analytics/breadcrumbs.ts | 21 --- .../pages/analytics_exploration/directive.tsx | 69 -------- .../pages/analytics_exploration/index.ts} | 3 +- .../pages/analytics_exploration/route.ts | 30 ---- .../pages/analytics_management/directive.tsx | 61 ------- .../pages/analytics_management/index.ts} | 5 +- .../pages/analytics_management/route.ts | 29 ---- .../application/datavisualizer/breadcrumbs.ts | 13 -- .../datavisualizer_selector.tsx | 4 +- .../application/datavisualizer/directive.tsx | 54 ------ .../datavisualizer/file_based/breadcrumbs.ts | 23 --- .../components/import_view/import_view.js | 2 +- .../components/results_links/results_links.js | 4 +- .../file_based/file_datavisualizer.tsx | 10 +- .../file_datavisualizer_directive.tsx | 68 -------- .../datavisualizer/file_based/index.ts | 2 +- .../application/datavisualizer/index.ts | 4 +- .../datavisualizer/index_based/breadcrumbs.ts | 27 --- .../actions_panel/actions_panel.tsx | 7 +- .../datavisualizer/index_based/directive.tsx | 61 ------- .../datavisualizer/index_based/index.ts | 3 +- .../datavisualizer/index_based/page.tsx | 7 +- .../datavisualizer/index_based/route.ts | 28 ---- .../application/explorer/breadcrumbs.ts | 23 --- .../public/application/explorer/explorer.js | 3 +- .../explorer/explorer_directive.tsx | 110 ------------ .../application/explorer/explorer_route.ts | 27 --- .../ml/public/application/explorer/index.ts | 7 +- .../plugins/ml/public/application/index.scss | 45 +++++ .../ml/public/application/jobs/breadcrumbs.ts | 112 ------------- .../{data_frame_analytics => jobs}/index.ts | 6 +- .../components/job_actions/results.js | 5 +- .../job_details/extract_job_details.js | 3 +- .../forecasts_table/forecasts_table.js | 3 +- .../jobs_list_view/jobs_list_view.js | 9 - .../application/jobs/jobs_list/directive.js | 59 ------- .../jobs/{index.js => jobs_list/index.ts} | 5 +- .../jobs/jobs_list/{jobs.js => jobs.tsx} | 17 +- .../job_creator/advanced_job_creator.ts | 10 +- .../new_job/common/job_creator/configs/job.ts | 2 +- .../new_job/common/job_creator/job_creator.ts | 16 +- .../common/job_creator/job_creator_factory.ts | 6 +- .../job_creator/multi_metric_job_creator.ts | 14 +- .../job_creator/population_job_creator.ts | 10 +- .../job_creator/single_metric_job_creator.ts | 10 +- .../new_job/common/job_creator/type_guards.ts | 2 +- .../common/job_creator/util/general.ts | 2 +- .../common/results_loader/results_loader.ts | 2 +- .../public/application/jobs/new_job/index.ts | 14 -- .../query_delay/query_delay_input.tsx | 2 +- .../advanced_section/advanced_section.tsx | 2 +- .../multi_metric_view/chart_grid.tsx | 2 +- .../components/population_view/chart_grid.tsx | 2 +- .../components/split_cards/split_cards.tsx | 2 +- .../components/split_field/description.tsx | 2 +- .../pick_fields_step/pick_fields.tsx | 2 +- .../datafeed_details/datafeed_details.tsx | 2 +- .../detector_chart/detector_chart.tsx | 2 +- .../pages/components/summary_step/summary.tsx | 2 +- .../components/time_range_step/time_range.tsx | 20 ++- .../components/validation_step/validation.tsx | 2 +- .../index_or_search/__test__/directive.js | 45 ----- .../pages/index_or_search/directive.tsx | 42 ----- .../new_job/pages/index_or_search/index.ts | 8 + .../preconfigured_job_redirect.ts | 7 +- .../new_job/pages/index_or_search/route.ts | 47 ------ .../pages/job_type/__test__/directive.js | 45 ----- .../jobs/new_job/pages/job_type/directive.tsx | 64 ------- .../jobs/new_job/pages/job_type/index.ts | 7 + .../jobs/new_job/pages/job_type/page.tsx | 40 ++--- .../jobs/new_job/pages/job_type/route.ts | 25 --- .../jobs/new_job/pages/new_job/directive.tsx | 76 --------- .../jobs/new_job/pages/new_job/index.ts | 7 + .../jobs/new_job/pages/new_job/page.tsx | 4 +- .../jobs/new_job/pages/new_job/route.ts | 65 -------- .../jobs/new_job/pages/new_job/wizard.tsx | 2 +- .../pages/new_job/wizard_horizontal_steps.tsx | 2 +- .../new_job/pages/new_job/wizard_steps.tsx | 4 +- .../new_job/recognize/__test__/directive.js | 45 ----- .../jobs/new_job/recognize/directive.tsx | 68 -------- .../jobs/new_job/recognize/index.ts | 7 + .../jobs/new_job/recognize/page.tsx | 8 +- .../jobs/new_job/recognize/resolvers.ts | 11 +- .../jobs/new_job/recognize/route.ts | 34 ---- .../jobs/new_job/utils/new_job_utils.ts | 50 +++--- .../application/overview/breadcrumbs.ts | 23 --- .../public/application/overview/directive.tsx | 38 ----- .../ml/public/application/overview/index.ts | 3 +- .../{ => application/routing}/breadcrumbs.ts | 15 +- .../ml/public/application/routing/index.ts | 7 + .../route.ts => routing/resolvers.ts} | 31 ++-- .../ml/public/application/routing/router.tsx | 71 ++++++++ .../routing/routes/access_denied.tsx | 36 ++++ .../analytics_job_exploration.tsx | 57 +++++++ .../analytics_jobs_list.tsx | 39 +++++ .../routes/data_frame_analytics/index.ts | 8 + .../routes/datavisualizer/datavisualizer.tsx | 40 +++++ .../routes/datavisualizer/file_based.tsx | 55 ++++++ .../routing/routes/datavisualizer/index.ts | 9 + .../routes/datavisualizer/index_based.tsx | 53 ++++++ .../application/routing/routes/explorer.tsx | 156 ++++++++++++++++++ .../application/routing/routes/index.ts | 15 ++ .../application/routing/routes/jobs_list.tsx | 40 +++++ .../routing/routes/new_job/index.ts | 11 ++ .../routes/new_job/index_or_search.tsx | 90 ++++++++++ .../routing/routes/new_job/job_type.tsx | 43 +++++ .../routing/routes/new_job/new_job.tsx | 33 ++++ .../routing/routes/new_job/recognize.tsx | 66 ++++++++ .../routing/routes/new_job/wizard.tsx | 121 ++++++++++++++ .../application/routing/routes/overview.tsx | 60 +++++++ .../routing/routes/settings/calendar_list.tsx | 56 +++++++ .../routes/settings/calendar_new_edit.tsx | 92 +++++++++++ .../routing/routes/settings/filter_list.tsx | 57 +++++++ .../routes/settings/filter_list_new_edit.tsx | 98 +++++++++++ .../routing/routes/settings/index.ts | 11 ++ .../routing/routes/settings/settings.tsx | 46 ++++++ .../routing/routes/timeseriesexplorer.tsx | 155 +++++++++++++++++ .../application/routing/use_resolver.ts | 70 ++++++++ .../application/services/job_service.d.ts | 4 +- .../application/services/job_service.js | 2 +- .../services/new_job_capabilities_service.ts | 21 +-- .../application/settings/breadcrumbs.ts | 86 ---------- .../__snapshots__/calendar_form.test.js.snap | 2 +- .../edit/calendar_form/calendar_form.js | 3 +- .../settings/calendars/edit/directive.tsx | 69 -------- .../settings/calendars/edit/index.ts | 2 +- .../settings/calendars/edit/new_calendar.d.ts | 2 +- .../settings/calendars/edit/new_calendar.js | 5 +- .../application/settings/calendars/index.ts | 8 + .../settings/calendars/list/directive.tsx | 58 ------- .../settings/calendars/list/index.ts | 2 +- .../table/__snapshots__/table.test.js.snap | 2 +- .../settings/calendars/list/table/table.js | 6 +- .../settings/filter_lists/edit/directive.tsx | 69 -------- .../filter_lists/edit/edit_filter_list.d.ts | 2 +- .../settings/filter_lists/edit/index.ts | 2 +- .../settings/filter_lists/index.ts | 4 +- .../settings/filter_lists/list/directive.tsx | 58 ------- .../settings/filter_lists/list/index.ts | 2 +- .../settings/filter_lists/list/table.js | 5 +- .../ml/public/application/settings/index.ts | 4 +- .../public/application/settings/settings.tsx | 7 +- .../settings/settings_directive.tsx | 60 ------- .../timeseriesexplorer/breadcrumbs.js | 27 --- .../application/timeseriesexplorer/index.js | 11 -- .../application/timeseriesexplorer/index.ts | 7 + .../timeseriesexplorer.d.ts | 15 ++ .../timeseriesexplorer/timeseriesexplorer.js | 2 +- .../timeseriesexplorer_directive.js | 111 ------------- .../timeseriesexplorer_route.js | 30 ---- .../ml/public/application/util/chart_utils.js | 3 +- .../application/util/chart_utils.test.js | 2 +- .../ml/public/application/util/index_utils.ts | 80 ++++++--- x-pack/legacy/plugins/ml/public/index.scss | 45 ----- x-pack/legacy/plugins/ml/public/index.ts | 12 ++ x-pack/legacy/plugins/ml/public/legacy.ts | 17 ++ x-pack/legacy/plugins/ml/public/plugin.ts | 46 ++++++ .../models/job_service/new_job/line_chart.ts | 8 + .../translations/translations/ja-JP.json | 3 - .../translations/translations/zh-CN.json | 3 - 179 files changed, 2198 insertions(+), 2437 deletions(-) create mode 100644 x-pack/legacy/plugins/ml/common/constants/app.ts rename x-pack/legacy/plugins/ml/{public/application/jobs/new_job/common/job_creator/util/constants.ts => common/constants/new_job.ts} (100%) create mode 100644 x-pack/legacy/plugins/ml/kibana.json delete mode 100644 x-pack/legacy/plugins/ml/public/application/app.js create mode 100644 x-pack/legacy/plugins/ml/public/application/app.tsx delete mode 100644 x-pack/legacy/plugins/ml/public/application/data_frame_analytics/breadcrumbs.ts delete mode 100644 x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/directive.tsx rename x-pack/legacy/plugins/ml/public/application/{jobs/jobs_list/index.js => data_frame_analytics/pages/analytics_exploration/index.ts} (88%) delete mode 100644 x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/route.ts delete mode 100644 x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/directive.tsx rename x-pack/legacy/plugins/ml/public/application/{settings/calendars/index.js => data_frame_analytics/pages/analytics_management/index.ts} (87%) delete mode 100644 x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/route.ts delete mode 100644 x-pack/legacy/plugins/ml/public/application/datavisualizer/breadcrumbs.ts delete mode 100644 x-pack/legacy/plugins/ml/public/application/datavisualizer/directive.tsx delete mode 100644 x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/breadcrumbs.ts delete mode 100644 x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/file_datavisualizer_directive.tsx delete mode 100644 x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/breadcrumbs.ts delete mode 100644 x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/directive.tsx delete mode 100644 x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/route.ts delete mode 100644 x-pack/legacy/plugins/ml/public/application/explorer/breadcrumbs.ts delete mode 100644 x-pack/legacy/plugins/ml/public/application/explorer/explorer_directive.tsx delete mode 100644 x-pack/legacy/plugins/ml/public/application/explorer/explorer_route.ts create mode 100644 x-pack/legacy/plugins/ml/public/application/index.scss delete mode 100644 x-pack/legacy/plugins/ml/public/application/jobs/breadcrumbs.ts rename x-pack/legacy/plugins/ml/public/application/{data_frame_analytics => jobs}/index.ts (56%) delete mode 100644 x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/directive.js rename x-pack/legacy/plugins/ml/public/application/jobs/{index.js => jobs_list/index.ts} (84%) rename x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/{jobs.js => jobs.tsx} (59%) delete mode 100644 x-pack/legacy/plugins/ml/public/application/jobs/new_job/index.ts delete mode 100644 x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/index_or_search/__test__/directive.js delete mode 100644 x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/index_or_search/directive.tsx create mode 100644 x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/index_or_search/index.ts delete mode 100644 x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/index_or_search/route.ts delete mode 100644 x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/job_type/__test__/directive.js delete mode 100644 x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/job_type/directive.tsx create mode 100644 x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/job_type/index.ts delete mode 100644 x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/job_type/route.ts delete mode 100644 x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/directive.tsx create mode 100644 x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/index.ts delete mode 100644 x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/route.ts delete mode 100644 x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/__test__/directive.js delete mode 100644 x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/directive.tsx create mode 100644 x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/index.ts delete mode 100644 x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/route.ts delete mode 100644 x-pack/legacy/plugins/ml/public/application/overview/breadcrumbs.ts delete mode 100644 x-pack/legacy/plugins/ml/public/application/overview/directive.tsx rename x-pack/legacy/plugins/ml/public/{ => application/routing}/breadcrumbs.ts (64%) create mode 100644 x-pack/legacy/plugins/ml/public/application/routing/index.ts rename x-pack/legacy/plugins/ml/public/application/{overview/route.ts => routing/resolvers.ts} (50%) create mode 100644 x-pack/legacy/plugins/ml/public/application/routing/router.tsx create mode 100644 x-pack/legacy/plugins/ml/public/application/routing/routes/access_denied.tsx create mode 100644 x-pack/legacy/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx create mode 100644 x-pack/legacy/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_jobs_list.tsx create mode 100644 x-pack/legacy/plugins/ml/public/application/routing/routes/data_frame_analytics/index.ts create mode 100644 x-pack/legacy/plugins/ml/public/application/routing/routes/datavisualizer/datavisualizer.tsx create mode 100644 x-pack/legacy/plugins/ml/public/application/routing/routes/datavisualizer/file_based.tsx create mode 100644 x-pack/legacy/plugins/ml/public/application/routing/routes/datavisualizer/index.ts create mode 100644 x-pack/legacy/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx create mode 100644 x-pack/legacy/plugins/ml/public/application/routing/routes/explorer.tsx create mode 100644 x-pack/legacy/plugins/ml/public/application/routing/routes/index.ts create mode 100644 x-pack/legacy/plugins/ml/public/application/routing/routes/jobs_list.tsx create mode 100644 x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/index.ts create mode 100644 x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx create mode 100644 x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/job_type.tsx create mode 100644 x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/new_job.tsx create mode 100644 x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/recognize.tsx create mode 100644 x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/wizard.tsx create mode 100644 x-pack/legacy/plugins/ml/public/application/routing/routes/overview.tsx create mode 100644 x-pack/legacy/plugins/ml/public/application/routing/routes/settings/calendar_list.tsx create mode 100644 x-pack/legacy/plugins/ml/public/application/routing/routes/settings/calendar_new_edit.tsx create mode 100644 x-pack/legacy/plugins/ml/public/application/routing/routes/settings/filter_list.tsx create mode 100644 x-pack/legacy/plugins/ml/public/application/routing/routes/settings/filter_list_new_edit.tsx create mode 100644 x-pack/legacy/plugins/ml/public/application/routing/routes/settings/index.ts create mode 100644 x-pack/legacy/plugins/ml/public/application/routing/routes/settings/settings.tsx create mode 100644 x-pack/legacy/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx create mode 100644 x-pack/legacy/plugins/ml/public/application/routing/use_resolver.ts delete mode 100644 x-pack/legacy/plugins/ml/public/application/settings/breadcrumbs.ts delete mode 100644 x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/directive.tsx create mode 100644 x-pack/legacy/plugins/ml/public/application/settings/calendars/index.ts delete mode 100644 x-pack/legacy/plugins/ml/public/application/settings/calendars/list/directive.tsx delete mode 100644 x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/directive.tsx delete mode 100644 x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/directive.tsx delete mode 100644 x-pack/legacy/plugins/ml/public/application/settings/settings_directive.tsx delete mode 100644 x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/breadcrumbs.js delete mode 100644 x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/index.js create mode 100644 x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/index.ts create mode 100644 x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.d.ts delete mode 100644 x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_directive.js delete mode 100644 x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_route.js delete mode 100644 x-pack/legacy/plugins/ml/public/index.scss create mode 100755 x-pack/legacy/plugins/ml/public/index.ts create mode 100644 x-pack/legacy/plugins/ml/public/legacy.ts create mode 100644 x-pack/legacy/plugins/ml/public/plugin.ts diff --git a/x-pack/legacy/plugins/ml/common/constants/app.ts b/x-pack/legacy/plugins/ml/common/constants/app.ts new file mode 100644 index 0000000000000..140a709b0c42b --- /dev/null +++ b/x-pack/legacy/plugins/ml/common/constants/app.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const API_BASE_PATH = '/api/transform/'; diff --git a/x-pack/legacy/plugins/ml/common/constants/feature_flags.ts b/x-pack/legacy/plugins/ml/common/constants/feature_flags.ts index 96a46c92cb602..48e88e79f9674 100644 --- a/x-pack/legacy/plugins/ml/common/constants/feature_flags.ts +++ b/x-pack/legacy/plugins/ml/common/constants/feature_flags.ts @@ -9,6 +9,5 @@ // indices and aliases exist. Based on that the final setting will be available // as an injectedVar on the client side and can be accessed like: // -// import chrome from 'ui/chrome'; -// const mlAnnotationsEnabled = chrome.getInjected('mlAnnotationsEnabled', false); + export const FEATURE_ANNOTATIONS_ENABLED = true; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/util/constants.ts b/x-pack/legacy/plugins/ml/common/constants/new_job.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/util/constants.ts rename to x-pack/legacy/plugins/ml/common/constants/new_job.ts diff --git a/x-pack/legacy/plugins/ml/common/types/kibana.ts b/x-pack/legacy/plugins/ml/common/types/kibana.ts index 86db2ce59d7e7..d647bd882162b 100644 --- a/x-pack/legacy/plugins/ml/common/types/kibana.ts +++ b/x-pack/legacy/plugins/ml/common/types/kibana.ts @@ -6,6 +6,8 @@ // custom edits or fixes for default kibana types which are incomplete +import { SavedObjectAttributes, SimpleSavedObject } from 'kibana/public'; + export type IndexPatternTitle = string; export type callWithRequestType = (action: string, params?: any) => Promise; @@ -14,3 +16,12 @@ export interface Route { id: string; k7Breadcrumbs: () => any; } + +export type IndexPatternSavedObject = SimpleSavedObject; +export type SavedSearchSavedObject = SimpleSavedObject; + +export function isSavedSearchSavedObject( + ss: SavedSearchSavedObject | null +): ss is SavedSearchSavedObject { + return ss !== null; +} diff --git a/x-pack/legacy/plugins/ml/common/util/job_utils.js b/x-pack/legacy/plugins/ml/common/util/job_utils.js index 999eb44b372bc..cef3475a9654f 100644 --- a/x-pack/legacy/plugins/ml/common/util/job_utils.js +++ b/x-pack/legacy/plugins/ml/common/util/job_utils.js @@ -12,7 +12,7 @@ import numeral from '@elastic/numeral'; import { ALLOWED_DATA_UNITS, JOB_ID_MAX_LENGTH } from '../constants/validation'; import { parseInterval } from './parse_interval'; import { maxLengthValidator } from './validators'; -import { CREATED_BY_LABEL } from '../../public/application/jobs/new_job/common/job_creator/util/constants'; +import { CREATED_BY_LABEL } from '../../common/constants/new_job'; // work out the default frequency based on the bucket_span in seconds export function calculateDatafeedFrequencyDefaultSeconds(bucketSpanSeconds) { diff --git a/x-pack/legacy/plugins/ml/index.ts b/x-pack/legacy/plugins/ml/index.ts index 9b42998c814fd..3078a0c812ff1 100755 --- a/x-pack/legacy/plugins/ml/index.ts +++ b/x-pack/legacy/plugins/ml/index.ts @@ -41,9 +41,9 @@ export const ml = (kibana: any) => { }), icon: 'plugins/ml/application/ml.svg', euiIconType: 'machineLearningApp', - main: 'plugins/ml/application/app', + main: 'plugins/ml/legacy', }, - styleSheetPaths: resolve(__dirname, 'public/index.scss'), + styleSheetPaths: resolve(__dirname, 'public/application/index.scss'), hacks: ['plugins/ml/application/hacks/toggle_app_link_in_nav'], savedObjectSchemas: { 'ml-telemetry': { diff --git a/x-pack/legacy/plugins/ml/kibana.json b/x-pack/legacy/plugins/ml/kibana.json new file mode 100644 index 0000000000000..f36b484818690 --- /dev/null +++ b/x-pack/legacy/plugins/ml/kibana.json @@ -0,0 +1,8 @@ +{ + "id": "ml", + "version": "0.0.1", + "kibanaVersion": "kibana", + "configPath": ["ml"], + "server": true, + "ui": true +} diff --git a/x-pack/legacy/plugins/ml/public/application/access_denied/index.tsx b/x-pack/legacy/plugins/ml/public/application/access_denied/index.tsx index 883754896487e..7e2d651439ae3 100644 --- a/x-pack/legacy/plugins/ml/public/application/access_denied/index.tsx +++ b/x-pack/legacy/plugins/ml/public/application/access_denied/index.tsx @@ -4,36 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import ReactDOM from 'react-dom'; -import uiRoutes from 'ui/routes'; -import { I18nContext } from 'ui/i18n'; -// @ts-ignore -import { uiModules } from 'ui/modules'; -import { AccessDeniedPage } from './page'; - -const module = uiModules.get('apps/ml', ['react']); - -const template = ``; - -uiRoutes.when('/access-denied', { - template, -}); - -module.directive('accessDenied', function() { - return { - scope: {}, - restrict: 'E', - link: async (scope: ng.IScope, element: ng.IAugmentedJQuery) => { - ReactDOM.render( - {React.createElement(AccessDeniedPage)}, - element[0] - ); - - element.on('$destroy', () => { - ReactDOM.unmountComponentAtNode(element[0]); - scope.$destroy(); - }); - }, - }; -}); +export { Page } from './page'; diff --git a/x-pack/legacy/plugins/ml/public/application/access_denied/page.tsx b/x-pack/legacy/plugins/ml/public/application/access_denied/page.tsx index 1c908e114cbeb..32b2ade5dc9dc 100644 --- a/x-pack/legacy/plugins/ml/public/application/access_denied/page.tsx +++ b/x-pack/legacy/plugins/ml/public/application/access_denied/page.tsx @@ -21,7 +21,7 @@ import { } from '@elastic/eui'; import { NavigationMenu } from '../components/navigation_menu'; -export const AccessDeniedPage = () => ( +export const Page = () => ( diff --git a/x-pack/legacy/plugins/ml/public/application/app.js b/x-pack/legacy/plugins/ml/public/application/app.js deleted file mode 100644 index 722e2c8d05e9b..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/app.js +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - - -import 'uiExports/savedObjectTypes'; - -import 'ui/autoload/all'; - -// needed to make syntax highlighting work in ace editors -import 'ace'; - -import './access_denied'; -import './jobs'; -import './overview'; -import './services/calendar_service'; -import './data_frame_analytics'; -import './datavisualizer'; -import './explorer'; -import './timeseriesexplorer'; -import './components/navigation_menu'; -import './components/loading_indicator'; -import './settings'; - -import uiRoutes from 'ui/routes'; - -if (typeof uiRoutes.enable === 'function') { - uiRoutes.enable(); -} - -uiRoutes - .otherwise({ - redirectTo: '/overview' - }); diff --git a/x-pack/legacy/plugins/ml/public/application/app.tsx b/x-pack/legacy/plugins/ml/public/application/app.tsx new file mode 100644 index 0000000000000..c790f10725716 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/app.tsx @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import ReactDOM from 'react-dom'; + +import 'uiExports/savedObjectTypes'; + +import 'ui/autoload/all'; + +// needed to make syntax highlighting work in ace editors +import 'ace'; + +import { AppMountContext, AppMountParameters } from 'kibana/public'; +import { + IndexPatternsContract, + Plugin as DataPlugin, +} from '../../../../../../src/plugins/data/public'; + +import { KibanaConfigTypeFix } from './contexts/kibana'; + +import { MlRouter } from './routing'; + +export interface MlDependencies extends AppMountParameters { + npData: ReturnType; + indexPatterns: IndexPatternsContract; +} + +interface AppProps { + context: AppMountContext; + indexPatterns: IndexPatternsContract; +} + +const App: FC = ({ context, indexPatterns }) => { + const config = (context.core.uiSettings as never) as KibanaConfigTypeFix; // TODO - make this UiSettingsClientContract, get rid of KibanaConfigTypeFix + + return ( + + ); +}; + +export const renderApp = (context: AppMountContext, { element, indexPatterns }: MlDependencies) => { + ReactDOM.render(, element); + + return () => ReactDOM.unmountComponentAtNode(element); +}; diff --git a/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js index 909abfd4abc23..640ae8f962eed 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js +++ b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js @@ -35,7 +35,6 @@ import { } from '@elastic/eui/lib/services'; import { formatDate } from '@elastic/eui/lib/services/format'; -import chrome from 'ui/chrome'; import { addItemToRecentlyAccessed } from '../../../util/recently_accessed'; import { ml } from '../../../services/ml_api_service'; @@ -206,7 +205,7 @@ const AnnotationsTable = injectI18n(class AnnotationsTable extends Component { const url = `?_g=${_g}&_a=${_a}`; addItemToRecentlyAccessed('timeseriesexplorer', job.job_id, url); - window.open(`${chrome.getBasePath()}/app/ml#/timeseriesexplorer${url}`, '_self'); + window.open(`#/timeseriesexplorer${url}`, '_self'); } onMouseOverRow = (record) => { diff --git a/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/links_menu.js b/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/links_menu.js index 19cd77655f97c..f237bcc2efc53 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/links_menu.js +++ b/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/links_menu.js @@ -205,7 +205,7 @@ export const LinksMenu = injectI18n(class LinksMenu extends Component { }); // Need to encode the _a parameter in case any entities contain unsafe characters such as '+'. - let path = `${chrome.getBasePath()}/app/ml#/timeseriesexplorer`; + let path = '#/timeseriesexplorer'; path += `?_g=${_g}&_a=${encodeURIComponent(_a)}`; window.open(path, '_blank'); } diff --git a/x-pack/legacy/plugins/ml/public/application/components/data_recognizer/data_recognizer.d.ts b/x-pack/legacy/plugins/ml/public/application/components/data_recognizer/data_recognizer.d.ts index e7d191a31e034..94a502e6eadde 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/data_recognizer/data_recognizer.d.ts +++ b/x-pack/legacy/plugins/ml/public/application/components/data_recognizer/data_recognizer.d.ts @@ -7,11 +7,11 @@ import { FC } from 'react'; import { IndexPattern } from 'ui/index_patterns'; -import { SavedSearch } from 'src/legacy/core_plugins/kibana/public/discover/types'; +import { SavedSearchSavedObject } from '../../../../common/types/kibana'; declare const DataRecognizer: FC<{ indexPattern: IndexPattern; - savedSearch?: SavedSearch; + savedSearch?: SavedSearchSavedObject | null; results: { count: number; onChange?: Function; diff --git a/x-pack/legacy/plugins/ml/public/application/components/data_recognizer/recognized_result.js b/x-pack/legacy/plugins/ml/public/application/components/data_recognizer/recognized_result.js index 6f511abf89e31..79b1b501c3829 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/data_recognizer/recognized_result.js +++ b/x-pack/legacy/plugins/ml/public/application/components/data_recognizer/recognized_result.js @@ -20,7 +20,7 @@ export const RecognizedResult = ({ indexPattern, savedSearch }) => { - const id = (savedSearch === undefined || savedSearch.id === undefined) ? + const id = (savedSearch === null) ? `index=${indexPattern.id}` : `savedSearchId=${savedSearch.id}`; diff --git a/x-pack/legacy/plugins/ml/public/application/components/navigation_menu/main_tabs.tsx b/x-pack/legacy/plugins/ml/public/application/components/navigation_menu/main_tabs.tsx index cff174eb5627f..5735faa9c6f52 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/navigation_menu/main_tabs.tsx +++ b/x-pack/legacy/plugins/ml/public/application/components/navigation_menu/main_tabs.tsx @@ -7,7 +7,6 @@ import React, { FC, useState } from 'react'; import { EuiTabs, EuiTab, EuiLink } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import chrome from 'ui/chrome'; import { TabId } from './navigation_menu'; export interface Tab { @@ -82,7 +81,7 @@ export const MainTabs: FC = ({ tabId, disableLinks }) => { return ( diff --git a/x-pack/legacy/plugins/ml/public/application/components/navigation_menu/tabs.tsx b/x-pack/legacy/plugins/ml/public/application/components/navigation_menu/tabs.tsx index 7014164ad9756..20fa2cca41231 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/navigation_menu/tabs.tsx +++ b/x-pack/legacy/plugins/ml/public/application/components/navigation_menu/tabs.tsx @@ -7,7 +7,6 @@ import React, { FC, useState } from 'react'; import { EuiTabs, EuiTab, EuiLink } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import chrome from 'ui/chrome'; import { Tab } from './main_tabs'; import { TabId } from './navigation_menu'; @@ -84,7 +83,7 @@ export const Tabs: FC = ({ tabId, mainTabId, disableLinks }) => { data-test-subj={ TAB_TEST_SUBJECT[id as TAB_TEST_SUBJECTS] + (id === selectedTabId ? ' selected' : '') } - href={`${chrome.getBasePath()}/app/ml#/${id}`} + href={`#/${id}`} key={`${id}-key`} color="text" > diff --git a/x-pack/legacy/plugins/ml/public/application/contexts/kibana/__mocks__/saved_search.ts b/x-pack/legacy/plugins/ml/public/application/contexts/kibana/__mocks__/saved_search.ts index 2bff760ed3711..cbbdaf410445e 100644 --- a/x-pack/legacy/plugins/ml/public/application/contexts/kibana/__mocks__/saved_search.ts +++ b/x-pack/legacy/plugins/ml/public/application/contexts/kibana/__mocks__/saved_search.ts @@ -4,14 +4,23 @@ * you may not use this file except in compliance with the Elastic License. */ -import { searchSourceMock } from '../../../../../../../../../src/legacy/ui/public/courier/search_source/mocks'; -import { SearchSourceContract } from '../../../../../../../../../src/legacy/ui/public/courier'; - -export const savedSearchMock = { +export const savedSearchMock: any = { id: 'the-saved-search-id', - title: 'the-saved-search-title', - searchSource: searchSourceMock as SearchSourceContract, - columns: [], - sort: [], - destroy: () => {}, + type: 'search', + attributes: { + title: 'the-saved-search-title', + kibanaSavedObjectMeta: { + searchSourceJSON: + '{"highlightAll":true,"version":true,"query":{"query":"foo : \\"bar\\" ","language":"kuery"},"filter":[],"indexRefName":"kibanaSavedObjectMeta.searchSourceJSON.index"}', + }, + }, + references: [ + { + name: 'kibanaSavedObjectMeta.searchSourceJSON.index', + type: 'index-pattern', + id: 'the-index-pattern-id', + }, + ], + migrationVersion: { search: '7.5.0' }, + error: undefined, }; diff --git a/x-pack/legacy/plugins/ml/public/application/contexts/kibana/kibana_context.ts b/x-pack/legacy/plugins/ml/public/application/contexts/kibana/kibana_context.ts index 00989245e20e7..9d0a3bc43e258 100644 --- a/x-pack/legacy/plugins/ml/public/application/contexts/kibana/kibana_context.ts +++ b/x-pack/legacy/plugins/ml/public/application/contexts/kibana/kibana_context.ts @@ -7,12 +7,11 @@ import React from 'react'; import { KibanaConfig } from 'src/legacy/server/kbn_server'; -import { SavedSearch } from 'src/legacy/core_plugins/kibana/public/discover/types'; - import { IndexPattern, IndexPatternsContract, } from '../../../../../../../../src/plugins/data/public'; +import { SavedSearchSavedObject } from '../../../../common/types/kibana'; // set() method is missing in original d.ts export interface KibanaConfigTypeFix extends KibanaConfig { @@ -21,8 +20,8 @@ export interface KibanaConfigTypeFix extends KibanaConfig { export interface KibanaContextValue { combinedQuery: any; - currentIndexPattern: IndexPattern; - currentSavedSearch: SavedSearch; + currentIndexPattern: IndexPattern; // TODO this should be IndexPattern or null + currentSavedSearch: SavedSearchSavedObject | null; indexPatterns: IndexPatternsContract; kibanaConfig: KibanaConfigTypeFix; } diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/breadcrumbs.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/breadcrumbs.ts deleted file mode 100644 index fde854b7f41c3..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/breadcrumbs.ts +++ /dev/null @@ -1,21 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; - -import { ML_BREADCRUMB } from '../../breadcrumbs'; - -export function getDataFrameAnalyticsBreadcrumbs() { - return [ - ML_BREADCRUMB, - { - text: i18n.translate('xpack.ml.dataFrameAnalyticsBreadcrumbs.dataFrameLabel', { - defaultMessage: 'Data Frame Analytics', - }), - href: '', - }, - ]; -} diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/directive.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/directive.tsx deleted file mode 100644 index 1d4ac85ae2e87..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/directive.tsx +++ /dev/null @@ -1,69 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import ReactDOM from 'react-dom'; - -// @ts-ignore -import { uiModules } from 'ui/modules'; -const module = uiModules.get('apps/ml', ['react']); - -import { I18nContext } from 'ui/i18n'; -import { IndexPatternsContract } from '../../../../../../../../../src/plugins/data/public'; - -import { InjectorService } from '../../../../../common/types/angular'; -import { createSearchItems } from '../../../jobs/new_job/utils/new_job_utils'; - -import { KibanaConfigTypeFix, KibanaContext } from '../../../contexts/kibana'; - -import { Page } from './page'; - -module.directive('mlDataFrameAnalyticsExploration', ($injector: InjectorService) => { - return { - scope: {}, - restrict: 'E', - link: (scope: ng.IScope, element: ng.IAugmentedJQuery) => { - const globalState = $injector.get('globalState'); - globalState.fetch(); - - const indexPatterns = $injector.get('indexPatterns'); - const kibanaConfig = $injector.get('config'); - const $route = $injector.get('$route'); - - const { indexPattern, savedSearch, combinedQuery } = createSearchItems( - kibanaConfig, - $route.current.locals.indexPattern, - $route.current.locals.savedSearch - ); - - const kibanaContext = { - combinedQuery, - currentIndexPattern: indexPattern, - currentSavedSearch: savedSearch, - indexPatterns, - kibanaConfig, - }; - - ReactDOM.render( - - - - - , - element[0] - ); - - element.on('$destroy', () => { - ReactDOM.unmountComponentAtNode(element[0]); - scope.$destroy(); - }); - }, - }; -}); diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/index.js b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/index.ts similarity index 88% rename from x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/index.js rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/index.ts index 3839017291326..7e2d651439ae3 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/index.js +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/index.ts @@ -4,5 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ - -import './directive'; +export { Page } from './page'; diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/route.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/route.ts deleted file mode 100644 index b705c604c190c..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/route.ts +++ /dev/null @@ -1,30 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import uiRoutes from 'ui/routes'; - -import { checkFullLicense } from '../../../license/check_license'; -import { checkGetJobsPrivilege } from '../../../privilege/check_privilege'; -import { - loadCurrentIndexPattern, - loadCurrentSavedSearch, - loadIndexPatterns, -} from '../../../util/index_utils'; -import { getDataFrameAnalyticsBreadcrumbs } from '../../breadcrumbs'; - -const template = ``; - -uiRoutes.when('/data_frame_analytics/exploration?', { - template, - k7Breadcrumbs: getDataFrameAnalyticsBreadcrumbs, - resolve: { - CheckLicense: checkFullLicense, - privileges: checkGetJobsPrivilege, - indexPattern: loadCurrentIndexPattern, - indexPatterns: loadIndexPatterns, - savedSearch: loadCurrentSavedSearch, - }, -}); diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/directive.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/directive.tsx deleted file mode 100644 index 5d97ed6dfcd3d..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/directive.tsx +++ /dev/null @@ -1,61 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import ReactDOM from 'react-dom'; - -// @ts-ignore -import { uiModules } from 'ui/modules'; -const module = uiModules.get('apps/ml', ['react']); - -import { I18nContext } from 'ui/i18n'; -import { InjectorService } from '../../../../../common/types/angular'; -import { createSearchItems } from '../../../jobs/new_job/utils/new_job_utils'; -import { IndexPatternsContract } from '../../../../../../../../../src/plugins/data/public'; - -import { KibanaConfigTypeFix, KibanaContext } from '../../../contexts/kibana'; - -import { Page } from './page'; - -module.directive('mlDataFrameAnalyticsManagement', ($injector: InjectorService) => { - return { - scope: {}, - restrict: 'E', - link: (scope: ng.IScope, element: ng.IAugmentedJQuery) => { - const indexPatterns = $injector.get('indexPatterns'); - const kibanaConfig = $injector.get('config'); - const $route = $injector.get('$route'); - - const { indexPattern, savedSearch, combinedQuery } = createSearchItems( - kibanaConfig, - $route.current.locals.indexPattern, - $route.current.locals.savedSearch - ); - - const kibanaContext = { - combinedQuery, - currentIndexPattern: indexPattern, - currentSavedSearch: savedSearch, - indexPatterns, - kibanaConfig, - }; - - ReactDOM.render( - - - - - , - element[0] - ); - - element.on('$destroy', () => { - ReactDOM.unmountComponentAtNode(element[0]); - scope.$destroy(); - }); - }, - }; -}); diff --git a/x-pack/legacy/plugins/ml/public/application/settings/calendars/index.js b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/index.ts similarity index 87% rename from x-pack/legacy/plugins/ml/public/application/settings/calendars/index.js rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/index.ts index bcc62f4c5b10e..7e2d651439ae3 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/calendars/index.js +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/index.ts @@ -4,7 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ - - -import './list'; -import './edit'; +export { Page } from './page'; diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/route.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/route.ts deleted file mode 100644 index 89e02eb420639..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/route.ts +++ /dev/null @@ -1,29 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import uiRoutes from 'ui/routes'; - -import { checkFullLicense } from '../../../license/check_license'; -import { checkGetJobsPrivilege } from '../../../privilege/check_privilege'; -import { loadMlServerInfo } from '../../../services/ml_server_info'; -import { getMlNodeCount } from '../../../ml_nodes_check/check_ml_nodes'; -import { loadCurrentIndexPattern, loadCurrentSavedSearch } from '../../../util/index_utils'; -import { getDataFrameAnalyticsBreadcrumbs } from '../../breadcrumbs'; - -const template = ``; - -uiRoutes.when('/data_frame_analytics/?', { - template, - k7Breadcrumbs: getDataFrameAnalyticsBreadcrumbs, - resolve: { - CheckLicense: checkFullLicense, - privileges: checkGetJobsPrivilege, - indexPattern: loadCurrentIndexPattern, - savedSearch: loadCurrentSavedSearch, - mlNodeCount: getMlNodeCount, - loadMlServerInfo, - }, -}); diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/breadcrumbs.ts b/x-pack/legacy/plugins/ml/public/application/datavisualizer/breadcrumbs.ts deleted file mode 100644 index a4d1fd37bc338..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/breadcrumbs.ts +++ /dev/null @@ -1,13 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { ML_BREADCRUMB, DATA_VISUALIZER_BREADCRUMB } from '../../breadcrumbs'; - -export function getDataVisualizerBreadcrumbs() { - // Whilst top level nav menu with tabs remains, - // use root ML breadcrumb. - return [ML_BREADCRUMB, DATA_VISUALIZER_BREADCRUMB]; -} diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx index c24061cb052b8..1727b1652c55d 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx @@ -57,7 +57,7 @@ export const DatavisualizerSelector: FC = () => { return ( - + @@ -145,7 +145,7 @@ export const DatavisualizerSelector: FC = () => { footer={ - -`; - -uiRoutes.when('/datavisualizer', { - template, - k7Breadcrumbs: getDataVisualizerBreadcrumbs, - resolve: { - CheckLicense: checkBasicLicense, - privileges: checkFindFileStructurePrivilege, - }, -}); - -import { DatavisualizerSelector } from './datavisualizer_selector'; - -module.directive('datavisualizerSelector', function() { - return { - scope: {}, - restrict: 'E', - link: (scope: ng.IScope, element: ng.IAugmentedJQuery) => { - ReactDOM.render( - - - , - element[0] - ); - - element.on('$destroy', () => { - ReactDOM.unmountComponentAtNode(element[0]); - scope.$destroy(); - }); - }, - }; -}); diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/breadcrumbs.ts b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/breadcrumbs.ts deleted file mode 100644 index e8dd89f5db264..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/breadcrumbs.ts +++ /dev/null @@ -1,23 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; -import { ML_BREADCRUMB, DATA_VISUALIZER_BREADCRUMB } from '../../../breadcrumbs'; - -export function getFileDataVisualizerBreadcrumbs() { - // Whilst top level nav menu with tabs remains, - // use root ML breadcrumb. - return [ - ML_BREADCRUMB, - DATA_VISUALIZER_BREADCRUMB, - { - text: i18n.translate('xpack.ml.dataVisualizer.fileBasedLabel', { - defaultMessage: 'File', - }), - href: '', - }, - ]; -} diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_view/import_view.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_view/import_view.js index c89f618aa835b..b50eef363847e 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_view/import_view.js +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_view/import_view.js @@ -359,7 +359,7 @@ export class ImportView extends Component { } async loadIndexPatternNames() { - await loadIndexPatterns(); + await loadIndexPatterns(this.props.indexPatterns); const indexPatternNames = getIndexPatternNames(); this.setState({ indexPatternNames }); } diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/results_links/results_links.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/results_links/results_links.js index 30e91783fae2c..20b997582c3f9 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/results_links/results_links.js +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/results_links/results_links.js @@ -121,7 +121,7 @@ export class ResultsLinks extends Component { /> } description="" - href={`${uiChrome.getBasePath()}/app/ml#/jobs/new_job/step/job_type?index=${indexPatternId}${_g}`} + href={`#/jobs/new_job/step/job_type?index=${indexPatternId}${_g}`} /> } @@ -137,7 +137,7 @@ export class ResultsLinks extends Component { /> } description="" - href={`${uiChrome.getBasePath()}/app/ml#/jobs/new_job/datavisualizer?index=${indexPatternId}${_g}`} + href={`#/jobs/new_job/datavisualizer?index=${indexPatternId}${_g}`} /> } diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/file_datavisualizer.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/file_datavisualizer.tsx index 99e61d5937c1d..149e3d1818e64 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/file_datavisualizer.tsx +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/file_datavisualizer.tsx @@ -6,26 +6,22 @@ import React, { FC, Fragment } from 'react'; import { timefilter } from 'ui/timefilter'; -import { IndexPatternsContract } from '../../../../../../../../src/plugins/data/public'; import { KibanaConfigTypeFix } from '../../contexts/kibana'; import { NavigationMenu } from '../../components/navigation_menu'; +import { getIndexPatternsContract } from '../../util/index_utils'; // @ts-ignore import { FileDataVisualizerView } from './components/file_datavisualizer_view/index'; export interface FileDataVisualizerPageProps { - indexPatterns: IndexPatternsContract; kibanaConfig: KibanaConfigTypeFix; } -export const FileDataVisualizerPage: FC = ({ - indexPatterns, - kibanaConfig, -}) => { +export const FileDataVisualizerPage: FC = ({ kibanaConfig }) => { timefilter.disableTimeRangeSelector(); timefilter.disableAutoRefreshSelector(); - + const indexPatterns = getIndexPatternsContract(); return ( diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/file_datavisualizer_directive.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/file_datavisualizer_directive.tsx deleted file mode 100644 index 7ca2db041da29..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/file_datavisualizer_directive.tsx +++ /dev/null @@ -1,68 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import ReactDOM from 'react-dom'; -import { I18nContext } from 'ui/i18n'; - -// @ts-ignore -import { uiModules } from 'ui/modules'; - -const module = uiModules.get('apps/ml', ['react']); - -import uiRoutes from 'ui/routes'; -import { KibanaConfigTypeFix } from '../../contexts/kibana'; -import { getFileDataVisualizerBreadcrumbs } from './breadcrumbs'; -import { InjectorService } from '../../../../common/types/angular'; -import { checkBasicLicense } from '../../license/check_license'; -import { checkFindFileStructurePrivilege } from '../../privilege/check_privilege'; -import { getMlNodeCount } from '../../ml_nodes_check/check_ml_nodes'; -import { loadMlServerInfo } from '../../services/ml_server_info'; -import { loadIndexPatterns } from '../../util/index_utils'; -import { FileDataVisualizerPage, FileDataVisualizerPageProps } from './file_datavisualizer'; -import { IndexPatternsContract } from '../../../../../../../../src/plugins/data/public'; - -const template = ` -
- -`; - -uiRoutes.when('/filedatavisualizer/?', { - template, - k7Breadcrumbs: getFileDataVisualizerBreadcrumbs, - resolve: { - CheckLicense: checkBasicLicense, - privileges: checkFindFileStructurePrivilege, - indexPatterns: loadIndexPatterns, - mlNodeCount: getMlNodeCount, - loadMlServerInfo, - }, -}); - -module.directive('fileDatavisualizerPage', function($injector: InjectorService) { - return { - scope: {}, - restrict: 'E', - link: (scope: ng.IScope, element: ng.IAugmentedJQuery) => { - const indexPatterns = $injector.get('indexPatterns'); - const kibanaConfig = $injector.get('config'); - - const props: FileDataVisualizerPageProps = { - indexPatterns, - kibanaConfig, - }; - ReactDOM.render( - {React.createElement(FileDataVisualizerPage, props)}, - element[0] - ); - - element.on('$destroy', () => { - ReactDOM.unmountComponentAtNode(element[0]); - scope.$destroy(); - }); - }, - }; -}); diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/index.ts b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/index.ts index 15796ea9ff0bd..683d5e940aa7c 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/index.ts +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -import './file_datavisualizer_directive'; +export { FileDataVisualizerPage } from './file_datavisualizer'; diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index.ts b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index.ts index dcda3ec9879aa..770b48973b154 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index.ts +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index.ts @@ -4,6 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -import './directive'; -import './file_based'; -import './index_based'; +export { DatavisualizerSelector } from './datavisualizer_selector'; diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/breadcrumbs.ts b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/breadcrumbs.ts deleted file mode 100644 index aba45e04c638f..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/breadcrumbs.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; -import { - ML_BREADCRUMB, - DATA_VISUALIZER_BREADCRUMB, - // @ts-ignore -} from '../../../breadcrumbs'; - -export function getDataVisualizerBreadcrumbs() { - // Whilst top level nav menu with tabs remains, - // use root ML breadcrumb. - return [ - ML_BREADCRUMB, - DATA_VISUALIZER_BREADCRUMB, - { - text: i18n.translate('xpack.ml.dataFrameAnalyticsBreadcrumbs.indexLabel', { - defaultMessage: 'Index', - }), - href: '', - }, - ]; -} diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/actions_panel/actions_panel.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/actions_panel/actions_panel.tsx index fca2508cb5d14..0b68f7e096d85 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/actions_panel/actions_panel.tsx +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/actions_panel/actions_panel.tsx @@ -13,7 +13,6 @@ import { IndexPattern } from 'ui/index_patterns'; import { EuiPanel, EuiSpacer, EuiText, EuiTitle, EuiFlexGroup } from '@elastic/eui'; -import { useUiChromeContext } from '../../../../contexts/ui/use_ui_chrome_context'; import { CreateJobLinkCard } from '../../../../components/create_job_link_card'; import { DataRecognizer } from '../../../../components/data_recognizer'; @@ -31,12 +30,10 @@ export const ActionsPanel: FC = ({ indexPattern }) => { }, }; - const basePath = useUiChromeContext().getBasePath(); - function openAdvancedJobWizard() { // TODO - pass the search string to the advanced job page as well as the index pattern // (add in with new advanced job wizard?) - window.open(`${basePath}/app/ml#/jobs/new_job/advanced?index=${indexPattern}`, '_self'); + window.open(`#/jobs/new_job/advanced?index=${indexPattern}`, '_self'); } // Note we use display:none for the DataRecognizer section as it needs to be @@ -87,7 +84,7 @@ export const ActionsPanel: FC = ({ indexPattern }) => { 'Use the full range of options to create a job for more advanced use cases', })} onClick={openAdvancedJobWizard} - href={`${basePath}/app/ml#/jobs/new_job/advanced?index=${indexPattern}`} + href={`#/jobs/new_job/advanced?index=${indexPattern}`} /> ); diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/directive.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/directive.tsx deleted file mode 100644 index 5de7cb6b71acb..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/directive.tsx +++ /dev/null @@ -1,61 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import ReactDOM from 'react-dom'; - -// @ts-ignore -import { uiModules } from 'ui/modules'; -const module = uiModules.get('apps/ml', ['react']); - -import { I18nContext } from 'ui/i18n'; -import { IndexPatternsContract } from '../../../../../../../../src/plugins/data/public'; -import { InjectorService } from '../../../../common/types/angular'; - -import { KibanaConfigTypeFix, KibanaContext } from '../../contexts/kibana/kibana_context'; -import { createSearchItems } from '../../jobs/new_job/utils/new_job_utils'; - -import { Page } from './page'; - -module.directive('mlDataVisualizer', ($injector: InjectorService) => { - return { - scope: {}, - restrict: 'E', - link: (scope: ng.IScope, element: ng.IAugmentedJQuery) => { - const indexPatterns = $injector.get('indexPatterns'); - const kibanaConfig = $injector.get('config'); - const $route = $injector.get('$route'); - - const { indexPattern, savedSearch, combinedQuery } = createSearchItems( - kibanaConfig, - $route.current.locals.indexPattern, - $route.current.locals.savedSearch - ); - - const kibanaContext = { - combinedQuery, - currentIndexPattern: indexPattern, - currentSavedSearch: savedSearch, - indexPatterns, - kibanaConfig, - }; - - ReactDOM.render( - - - - - , - element[0] - ); - - element.on('$destroy', () => { - ReactDOM.unmountComponentAtNode(element[0]); - scope.$destroy(); - }); - }, - }; -}); diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/index.ts b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/index.ts index 8ef2e327a8984..7e2d651439ae3 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/index.ts +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/index.ts @@ -4,5 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -import './directive'; -import './route'; +export { Page } from './page'; diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/page.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/page.tsx index 99e128e954103..898c852fe50a5 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/page.tsx +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/page.tsx @@ -31,7 +31,7 @@ import { FullTimeRangeSelector } from '../../components/full_time_range_selector import { mlTimefilterRefresh$ } from '../../services/timefilter_refresh_service'; import { useKibanaContext, SavedSearchQuery } from '../../contexts/kibana'; import { kbnTypeToMLJobType } from '../../util/field_types_utils'; -import { timeBasedIndexCheck } from '../../util/index_utils'; +import { timeBasedIndexCheck, getQueryFromSavedSearch } from '../../util/index_utils'; import { TimeBuckets } from '../../util/time_buckets'; import { FieldRequestConfig, FieldVisConfig } from './common'; import { ActionsPanel } from './components/actions_panel'; @@ -173,9 +173,8 @@ export const Page: FC = () => { useEffect(() => { // Check for a saved search being passed in. - const searchSource = currentSavedSearch.searchSource; - const query = searchSource.getField('query'); - if (query !== undefined) { + if (currentSavedSearch !== null) { + const { query } = getQueryFromSavedSearch(currentSavedSearch); const queryLanguage = query.language as SEARCH_QUERY_LANGUAGE; const qryString = query.query; let qry; diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/route.ts b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/route.ts deleted file mode 100644 index ab4df73e720ea..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/route.ts +++ /dev/null @@ -1,28 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -// @ts-ignore -import uiRoutes from 'ui/routes'; -import { checkBasicLicense } from '../../license/check_license'; -import { checkGetJobsPrivilege } from '../../privilege/check_privilege'; -import { loadCurrentIndexPattern, loadCurrentSavedSearch } from '../../util/index_utils'; - -import { checkMlNodesAvailable } from '../../ml_nodes_check'; -import { getDataVisualizerBreadcrumbs } from './breadcrumbs'; - -const template = ``; - -uiRoutes.when('/jobs/new_job/datavisualizer', { - template, - k7Breadcrumbs: getDataVisualizerBreadcrumbs, - resolve: { - CheckLicense: checkBasicLicense, - privileges: checkGetJobsPrivilege, - indexPattern: loadCurrentIndexPattern, - savedSearch: loadCurrentSavedSearch, - checkMlNodesAvailable, - }, -}); diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/breadcrumbs.ts b/x-pack/legacy/plugins/ml/public/application/explorer/breadcrumbs.ts deleted file mode 100644 index c0dcd9e249b3b..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/explorer/breadcrumbs.ts +++ /dev/null @@ -1,23 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; -import { ML_BREADCRUMB, ANOMALY_DETECTION_BREADCRUMB } from '../../breadcrumbs'; - -export function getAnomalyExplorerBreadcrumbs() { - // Whilst top level nav menu with tabs remains, - // use root ML breadcrumb. - return [ - ML_BREADCRUMB, - ANOMALY_DETECTION_BREADCRUMB, - { - text: i18n.translate('xpack.ml.anomalyDetection.anomalyExplorerLabel', { - defaultMessage: 'Anomaly Explorer', - }), - href: '', - }, - ]; -} diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/explorer.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer.js index 50a57f634fd1b..d8a42064de4f2 100644 --- a/x-pack/legacy/plugins/ml/public/application/explorer/explorer.js +++ b/x-pack/legacy/plugins/ml/public/application/explorer/explorer.js @@ -93,7 +93,7 @@ function mapSwimlaneOptionsToEuiOptions(options) { } const ExplorerPage = ({ children, jobSelectorProps, resizeRef }) => ( -
+
{children} @@ -117,7 +117,6 @@ export const Explorer = injectI18n(injectObservablesAsProps( }; _unsubscribeAll = new Subject(); - // make sure dragSelect is only available if the mouse pointer is actually over a swimlane disableDragSelectOnMouseLeave = true; diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_directive.tsx b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_directive.tsx deleted file mode 100644 index b5d65fbf937e4..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_directive.tsx +++ /dev/null @@ -1,110 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -/* - * AngularJS directive wrapper for rendering Anomaly Explorer's React component. - */ - -import React from 'react'; -import ReactDOM from 'react-dom'; - -import { Subscription } from 'rxjs'; - -import { IRootElementService, IRootScopeService, IScope } from 'angular'; - -// @ts-ignore -import { uiModules } from 'ui/modules'; -const module = uiModules.get('apps/ml'); - -import { I18nContext } from 'ui/i18n'; -import { State } from 'ui/state_management/state'; -import { AppState as IAppState, AppStateClass } from 'ui/state_management/app_state'; - -import { jobSelectServiceFactory } from '../components/job_selector/job_select_service_utils'; - -import { interval$ } from '../components/controls/select_interval'; -import { severity$ } from '../components/controls/select_severity'; -import { showCharts$ } from '../components/controls/checkbox_showcharts'; -import { subscribeAppStateToObservable } from '../util/app_state_utils'; - -import { Explorer } from './explorer'; -import { explorerService } from './explorer_dashboard_service'; -import { getExplorerDefaultAppState, ExplorerAppState } from './reducers'; - -interface ExplorerScope extends IScope { - appState: IAppState; -} - -module.directive('mlAnomalyExplorer', function( - globalState: State, - $rootScope: IRootScopeService, - AppState: AppStateClass -) { - function link($scope: ExplorerScope, element: IRootElementService) { - const subscriptions = new Subscription(); - - const { jobSelectService$, unsubscribeFromGlobalState } = jobSelectServiceFactory(globalState); - - ReactDOM.render( - - - , - element[0] - ); - - // Initialize the AppState in which to store swimlane and filter settings. - // AppState is used to store state in the URL. - $scope.appState = new AppState(getExplorerDefaultAppState()); - const { mlExplorerFilter, mlExplorerSwimlane } = $scope.appState; - - // Pass the current URL AppState on to anomaly explorer's reactive state. - // After this hand-off, the appState stored in explorerState$ is the single - // source of truth. - explorerService.setAppState({ mlExplorerSwimlane, mlExplorerFilter }); - - // Now that appState in explorerState$ is the single source of truth, - // subscribe to it and update the actual URL appState on changes. - subscriptions.add( - explorerService.appState$.subscribe((appState: ExplorerAppState) => { - $scope.appState.fetch(); - $scope.appState.mlExplorerFilter = appState.mlExplorerFilter; - $scope.appState.mlExplorerSwimlane = appState.mlExplorerSwimlane; - $scope.appState.save(); - $scope.$applyAsync(); - }) - ); - - subscriptions.add( - subscribeAppStateToObservable(AppState, 'mlShowCharts', showCharts$, () => - $rootScope.$applyAsync() - ) - ); - subscriptions.add( - subscribeAppStateToObservable(AppState, 'mlSelectInterval', interval$, () => - $rootScope.$applyAsync() - ) - ); - subscriptions.add( - subscribeAppStateToObservable(AppState, 'mlSelectSeverity', severity$, () => - $rootScope.$applyAsync() - ) - ); - - element.on('$destroy', () => { - ReactDOM.unmountComponentAtNode(element[0]); - $scope.$destroy(); - subscriptions.unsubscribe(); - unsubscribeFromGlobalState(); - }); - } - - return { link }; -}); diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_route.ts b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_route.ts deleted file mode 100644 index a061176a5ef5b..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_route.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import uiRoutes from 'ui/routes'; - -import '../components/controls'; - -import { checkFullLicense } from '../license/check_license'; -import { checkGetJobsPrivilege } from '../privilege/check_privilege'; -import { mlJobService } from '../services/job_service'; -import { loadIndexPatterns } from '../util/index_utils'; - -import { getAnomalyExplorerBreadcrumbs } from './breadcrumbs'; - -uiRoutes.when('/explorer/?', { - template: ``, - k7Breadcrumbs: getAnomalyExplorerBreadcrumbs, - resolve: { - CheckLicense: checkFullLicense, - privileges: checkGetJobsPrivilege, - indexPatterns: loadIndexPatterns, - jobs: mlJobService.loadJobsWrapper, - }, -}); diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/index.ts b/x-pack/legacy/plugins/ml/public/application/explorer/index.ts index edc25565daa9f..1dd9b76b8c20c 100644 --- a/x-pack/legacy/plugins/ml/public/application/explorer/index.ts +++ b/x-pack/legacy/plugins/ml/public/application/explorer/index.ts @@ -4,9 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -import '../explorer/explorer_dashboard_service'; -import '../explorer/explorer_directive'; -import '../explorer/explorer_route'; -import '../explorer/explorer_charts'; -import '../explorer/select_limit'; -import '../components/job_selector'; +export { Explorer } from './explorer'; diff --git a/x-pack/legacy/plugins/ml/public/application/index.scss b/x-pack/legacy/plugins/ml/public/application/index.scss new file mode 100644 index 0000000000000..dbcdf288b6a85 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/index.scss @@ -0,0 +1,45 @@ +// Should import both the EUI constants and any Kibana ones that are considered global +@import 'src/legacy/ui/public/styles/styling_constants'; + +// ML has it's own variables for coloring +@import 'variables'; + +// Kibana management page ML section +#kibanaManagementMLSection { + @import 'management/index'; +} + +// Protect the rest of Kibana from ML generic namespacing +// SASSTODO: Prefix ml selectors instead +#ml-app { + // App level + @import 'app'; + + // Sub applications + @import 'data_frame_analytics/index'; + @import 'datavisualizer/index'; + @import 'explorer/index'; // SASSTODO: This file needs to be rewritten + @import 'jobs/index'; // SASSTODO: This collection of sass files has multiple problems + @import 'overview/index'; + @import 'settings/index'; + @import 'timeseriesexplorer/index'; + + // Components + @import 'components/annotations/annotation_description_list/index'; // SASSTODO: This file overwrites EUI directly + @import 'components/anomalies_table/index'; // SASSTODO: This file overwrites EUI directly + @import 'components/chart_tooltip/index'; + @import 'components/controls/index'; + @import 'components/entity_cell/index'; + @import 'components/field_title_bar/index'; + @import 'components/field_type_icon/index'; + @import 'components/influencers_list/index'; + @import 'components/items_grid/index'; + @import 'components/job_selector/index'; + @import 'components/loading_indicator/index'; // SASSTODO: This component should be replaced with EuiLoadingSpinner + @import 'components/navigation_menu/index'; + @import 'components/rule_editor/index'; // SASSTODO: This file overwrites EUI directly + @import 'components/stats_bar/index'; + + // Hacks are last so they can overwrite anything above if needed + @import 'hacks'; +} diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/breadcrumbs.ts b/x-pack/legacy/plugins/ml/public/application/jobs/breadcrumbs.ts deleted file mode 100644 index f2954548ea547..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/jobs/breadcrumbs.ts +++ /dev/null @@ -1,112 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; -import { Breadcrumb } from 'ui/chrome'; -import { - ANOMALY_DETECTION_BREADCRUMB, - DATA_VISUALIZER_BREADCRUMB, - ML_BREADCRUMB, -} from '../../breadcrumbs'; - -export function getJobManagementBreadcrumbs(): Breadcrumb[] { - // Whilst top level nav menu with tabs remains, - // use root ML breadcrumb. - return [ - ML_BREADCRUMB, - ANOMALY_DETECTION_BREADCRUMB, - { - text: i18n.translate('xpack.ml.anomalyDetection.jobManagementLabel', { - defaultMessage: 'Job Management', - }), - href: '', - }, - ]; -} - -export function getCreateJobBreadcrumbs(): Breadcrumb[] { - return [ - ML_BREADCRUMB, - ANOMALY_DETECTION_BREADCRUMB, - { - text: i18n.translate('xpack.ml.jobsBreadcrumbs.createJobLabel', { - defaultMessage: 'Create job', - }), - href: '#/jobs/new_job', - }, - ]; -} - -export function getCreateSingleMetricJobBreadcrumbs(): Breadcrumb[] { - return [ - ...getCreateJobBreadcrumbs(), - { - text: i18n.translate('xpack.ml.jobsBreadcrumbs.singleMetricLabel', { - defaultMessage: 'Single metric', - }), - href: '', - }, - ]; -} - -export function getCreateMultiMetricJobBreadcrumbs(): Breadcrumb[] { - return [ - ...getCreateJobBreadcrumbs(), - { - text: i18n.translate('xpack.ml.jobsBreadcrumbs.multiMetricLabel', { - defaultMessage: 'Multi metric', - }), - href: '', - }, - ]; -} - -export function getCreatePopulationJobBreadcrumbs(): Breadcrumb[] { - return [ - ...getCreateJobBreadcrumbs(), - { - text: i18n.translate('xpack.ml.jobsBreadcrumbs.populationLabel', { - defaultMessage: 'Population', - }), - href: '', - }, - ]; -} - -export function getAdvancedJobConfigurationBreadcrumbs(): Breadcrumb[] { - return [ - ...getCreateJobBreadcrumbs(), - { - text: i18n.translate('xpack.ml.jobsBreadcrumbs.advancedConfigurationLabel', { - defaultMessage: 'Advanced configuration', - }), - href: '', - }, - ]; -} - -export function getCreateRecognizerJobBreadcrumbs($routeParams: any): Breadcrumb[] { - return [ - ...getCreateJobBreadcrumbs(), - { - text: $routeParams.id, - href: '', - }, - ]; -} - -export function getDataVisualizerIndexOrSearchBreadcrumbs(): Breadcrumb[] { - return [ - ML_BREADCRUMB, - DATA_VISUALIZER_BREADCRUMB, - { - text: i18n.translate('xpack.ml.jobsBreadcrumbs.selectIndexOrSearchLabel', { - defaultMessage: 'Select index or search', - }), - href: '', - }, - ]; -} diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/index.ts similarity index 56% rename from x-pack/legacy/plugins/ml/public/application/data_frame_analytics/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/index.ts index 7363c5a27f64b..0bb30d4f76de7 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/index.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/index.ts @@ -4,7 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -import './pages/analytics_exploration/directive'; -import './pages/analytics_exploration/route'; -import './pages/analytics_management/directive'; -import './pages/analytics_management/route'; +export { JobsPage } from './jobs_list'; +export {} from './new_job'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_actions/results.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_actions/results.js index 5ec407f7f054e..d78cb37108391 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_actions/results.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_actions/results.js @@ -7,20 +7,20 @@ import PropTypes from 'prop-types'; import React from 'react'; +import chrome from 'ui/chrome'; import { EuiButtonIcon, EuiToolTip, } from '@elastic/eui'; -import chrome from 'ui/chrome'; import { mlJobService } from '../../../../services/job_service'; import { injectI18n } from '@kbn/i18n/react'; export function getLink(location, jobs) { const resultsPageUrl = mlJobService.createResultsUrlForJobs(jobs, location); - return `${chrome.getBasePath()}/app/${resultsPageUrl}`; + return `${chrome.getBasePath()}/app/ml${resultsPageUrl}`; } function ResultLinksUI({ jobs, intl }) { @@ -39,6 +39,7 @@ function ResultLinksUI({ jobs, intl }) { const singleMetricVisible = (jobs.length < 2); const singleMetricEnabled = (jobs.length === 1 && jobs[0].isSingleMetricViewerJob); const jobActionsDisabled = (jobs.length === 1 && jobs[0].deleting === true); + return ( {(singleMetricVisible) && diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/extract_job_details.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/extract_job_details.js index 028e6a10d6abc..9b301200c76f2 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/extract_job_details.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/extract_job_details.js @@ -6,7 +6,6 @@ import React from 'react'; import { EuiLink } from '@elastic/eui'; -import chrome from 'ui/chrome'; import { detectorToString } from '../../../../util/string_utils'; import { formatValues, filterObjects } from './format_values'; import { i18n } from '@kbn/i18n'; @@ -62,7 +61,7 @@ export function extractJobDetails(job) { if (job.calendars) { calendars.items = job.calendars.map(c => [ '', - {c}, + {c}, ]); // remove the calendars list from the general section // so not to show it twice. diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/forecasts_table/forecasts_table.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/forecasts_table/forecasts_table.js index 3df869174c146..8ae024e68460a 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/forecasts_table/forecasts_table.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/forecasts_table/forecasts_table.js @@ -23,7 +23,6 @@ import { EuiLoadingSpinner } from '@elastic/eui'; import { formatDate, formatNumber } from '@elastic/eui/lib/services/format'; -import chrome from 'ui/chrome'; import { FORECAST_REQUEST_STATE } from '../../../../../../../common/constants/states'; import { addItemToRecentlyAccessed } from '../../../../../util/recently_accessed'; @@ -128,7 +127,7 @@ class ForecastsTableUI extends Component { const url = `?_g=${_g}&_a=${_a}`; addItemToRecentlyAccessed('timeseriesexplorer', this.props.job.job_id, url); - window.open(`${chrome.getBasePath()}/app/ml#/timeseriesexplorer${url}`, '_self'); + window.open(`#/timeseriesexplorer${url}`, '_self'); } render() { diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js index fc07d4d2a0294..effc54c228130 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js @@ -67,15 +67,6 @@ export class JobsListView extends Component { if (this.props.isManagementTable === true) { this.refreshJobSummaryList(true); } else { - // The advanced job wizard is still angularjs based and triggers - // broadcast events which it expects the jobs list to be subscribed to. - this.props.angularWrapperScope.$on('jobsUpdated', () => { - this.refreshJobSummaryList(true); - }); - this.props.angularWrapperScope.$on('openCreateWatchWindow', (e, job) => { - this.showCreateWatchFlyout(job.job_id); - }); - timefilter.disableTimeRangeSelector(); timefilter.enableAutoRefreshSelector(); diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/directive.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/directive.js deleted file mode 100644 index f549ec3826cb5..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/directive.js +++ /dev/null @@ -1,59 +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; - * you may not use this file except in compliance with the Elastic License. - */ - - -import ReactDOM from 'react-dom'; -import React from 'react'; - -import { uiModules } from 'ui/modules'; -const module = uiModules.get('apps/ml', ['react']); - -import { loadIndexPatterns } from '../../util/index_utils'; -import { checkFullLicense } from '../../license/check_license'; -import { checkGetJobsPrivilege } from '../../privilege/check_privilege'; -import { getMlNodeCount } from '../../ml_nodes_check/check_ml_nodes'; -import { getJobManagementBreadcrumbs } from '../../jobs/breadcrumbs'; -import { loadMlServerInfo } from '../../services/ml_server_info'; - -import uiRoutes from 'ui/routes'; - -const template = ``; - -uiRoutes - .when('/jobs/?', { - template, - k7Breadcrumbs: getJobManagementBreadcrumbs, - resolve: { - CheckLicense: checkFullLicense, - indexPatterns: loadIndexPatterns, - privileges: checkGetJobsPrivilege, - mlNodeCount: getMlNodeCount, - loadMlServerInfo, - } - }); - -import { JobsPage } from './jobs'; -import { I18nContext } from 'ui/i18n'; - -module.directive('jobsPage', function () { - return { - scope: {}, - restrict: 'E', - link: (scope, element) => { - ReactDOM.render( - - - , - element[0] - ); - - element.on('$destroy', () => { - ReactDOM.unmountComponentAtNode(element[0]); - scope.$destroy(); - }); - } - }; -}); diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/index.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/index.ts similarity index 84% rename from x-pack/legacy/plugins/ml/public/application/jobs/index.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/index.ts index 1ade8752d6721..0b70e6b3c9352 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/index.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/index.ts @@ -4,7 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ - - -import './jobs_list'; -import './new_job'; +export { JobsPage } from './jobs'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/jobs.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/jobs.tsx similarity index 59% rename from x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/jobs.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/jobs.tsx index 21c184cdcd298..f820372e20c09 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/jobs.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/jobs.tsx @@ -4,15 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { FC } from 'react'; import { NavigationMenu } from '../../components/navigation_menu'; +// @ts-ignore import { JobsListView } from './components/jobs_list_view'; -export const JobsPage = (props) => ( - <> - - - -); +export const JobsPage: FC<{ props?: any }> = props => { + return ( +
+ + +
+ ); +}; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/advanced_job_creator.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/advanced_job_creator.ts index 22aebc2b88a88..d8917db7a33ff 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/advanced_job_creator.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/advanced_job_creator.ts @@ -4,14 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedSearch } from 'src/legacy/core_plugins/kibana/public/discover/types'; import { IndexPattern } from 'ui/index_patterns'; +import { SavedSearchSavedObject } from '../../../../../../common/types/kibana'; import { JobCreator } from './job_creator'; import { Field, Aggregation, SplitField } from '../../../../../../common/types/fields'; import { Job, Datafeed, Detector, CustomRule } from './configs'; import { createBasicDetector } from './util/default_configs'; -import { JOB_TYPE } from './util/constants'; +import { JOB_TYPE } from '../../../../../../common/constants/new_job'; import { getRichDetectors } from './util/general'; import { isValidJson } from '../../../../../../common/util/validation_utils'; import { ml } from '../../../../services/ml_api_service'; @@ -32,7 +32,11 @@ export class AdvancedJobCreator extends JobCreator { private _richDetectors: RichDetector[] = []; private _queryString: string; - constructor(indexPattern: IndexPattern, savedSearch: SavedSearch, query: object) { + constructor( + indexPattern: IndexPattern, + savedSearch: SavedSearchSavedObject | null, + query: object + ) { super(indexPattern, savedSearch, query); this._queryString = JSON.stringify(this._datafeed_config.query); diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/configs/job.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/configs/job.ts index 4960492eabeb3..3246f8ae4b31a 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/configs/job.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/configs/job.ts @@ -5,7 +5,7 @@ */ import { UrlConfig } from '../../../../../../../common/types/custom_urls'; -import { CREATED_BY_LABEL } from '../util/constants'; +import { CREATED_BY_LABEL } from '../../../../../../../common/constants/new_job'; export type JobId = string; export type BucketSpan = string; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts index e11cebe0383cd..4707eff8d844e 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedSearch } from 'src/legacy/core_plugins/kibana/public/discover/types'; import { IndexPattern } from 'ui/index_patterns'; +import { SavedSearchSavedObject } from '../../../../../../common/types/kibana'; import { UrlConfig } from '../../../../../../common/types/custom_urls'; import { IndexPatternTitle } from '../../../../../../common/types/kibana'; import { ML_JOB_AGGREGATION } from '../../../../../../common/constants/aggregation_types'; @@ -15,7 +15,11 @@ import { Aggregation, Field } from '../../../../../../common/types/fields'; import { createEmptyJob, createEmptyDatafeed } from './util/default_configs'; import { mlJobService } from '../../../../services/job_service'; import { JobRunner, ProgressSubscriber } from '../job_runner'; -import { JOB_TYPE, CREATED_BY_LABEL, SHARED_RESULTS_INDEX_NAME } from './util/constants'; +import { + JOB_TYPE, + CREATED_BY_LABEL, + SHARED_RESULTS_INDEX_NAME, +} from '../../../../../../common/constants/new_job'; import { isSparseDataJob } from './util/general'; import { parseInterval } from '../../../../../../common/util/parse_interval'; import { Calendar } from '../../../../../../common/types/calendars'; @@ -24,7 +28,7 @@ import { mlCalendarService } from '../../../../services/calendar_service'; export class JobCreator { protected _type: JOB_TYPE = JOB_TYPE.SINGLE_METRIC; protected _indexPattern: IndexPattern; - protected _savedSearch: SavedSearch; + protected _savedSearch: SavedSearchSavedObject | null; protected _indexPatternTitle: IndexPatternTitle = ''; protected _job_config: Job; protected _calendars: Calendar[]; @@ -44,7 +48,11 @@ export class JobCreator { stop: boolean; } = { stop: false }; - constructor(indexPattern: IndexPattern, savedSearch: SavedSearch, query: object) { + constructor( + indexPattern: IndexPattern, + savedSearch: SavedSearchSavedObject | null, + query: object + ) { this._indexPattern = indexPattern; this._savedSearch = savedSearch; this._indexPatternTitle = indexPattern.title; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator_factory.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator_factory.ts index 375e112ed46fa..4ffcd1b06ca47 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator_factory.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator_factory.ts @@ -4,18 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedSearch } from 'src/legacy/core_plugins/kibana/public/discover/types'; import { IndexPattern } from 'ui/index_patterns'; +import { SavedSearchSavedObject } from '../../../../../../common/types/kibana'; import { SingleMetricJobCreator } from './single_metric_job_creator'; import { MultiMetricJobCreator } from './multi_metric_job_creator'; import { PopulationJobCreator } from './population_job_creator'; import { AdvancedJobCreator } from './advanced_job_creator'; -import { JOB_TYPE } from './util/constants'; +import { JOB_TYPE } from '../../../../../../common/constants/new_job'; export const jobCreatorFactory = (jobType: JOB_TYPE) => ( indexPattern: IndexPattern, - savedSearch: SavedSearch, + savedSearch: SavedSearchSavedObject | null, query: object ) => { let jc; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/multi_metric_job_creator.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/multi_metric_job_creator.ts index fea328acb58b3..e86ee09d234f1 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/multi_metric_job_creator.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/multi_metric_job_creator.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedSearch } from 'src/legacy/core_plugins/kibana/public/discover/types'; import { IndexPattern } from 'ui/index_patterns'; +import { SavedSearchSavedObject } from '../../../../../../common/types/kibana'; import { JobCreator } from './job_creator'; import { Field, @@ -15,7 +15,11 @@ import { } from '../../../../../../common/types/fields'; import { Job, Datafeed, Detector } from './configs'; import { createBasicDetector } from './util/default_configs'; -import { JOB_TYPE, CREATED_BY_LABEL, DEFAULT_MODEL_MEMORY_LIMIT } from './util/constants'; +import { + JOB_TYPE, + CREATED_BY_LABEL, + DEFAULT_MODEL_MEMORY_LIMIT, +} from '../../../../../../common/constants/new_job'; import { ml } from '../../../../services/ml_api_service'; import { getRichDetectors } from './util/general'; @@ -26,7 +30,11 @@ export class MultiMetricJobCreator extends JobCreator { private _lastEstimatedModelMemoryLimit = DEFAULT_MODEL_MEMORY_LIMIT; protected _type: JOB_TYPE = JOB_TYPE.MULTI_METRIC; - constructor(indexPattern: IndexPattern, savedSearch: SavedSearch, query: object) { + constructor( + indexPattern: IndexPattern, + savedSearch: SavedSearchSavedObject | null, + query: object + ) { super(indexPattern, savedSearch, query); this.createdBy = CREATED_BY_LABEL.MULTI_METRIC; } diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/population_job_creator.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/population_job_creator.ts index 9e9ccf8ab63e4..8fcd03982424d 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/population_job_creator.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/population_job_creator.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedSearch } from 'src/legacy/core_plugins/kibana/public/discover/types'; import { IndexPattern } from 'ui/index_patterns'; +import { SavedSearchSavedObject } from '../../../../../../common/types/kibana'; import { JobCreator } from './job_creator'; import { Field, @@ -15,7 +15,7 @@ import { } from '../../../../../../common/types/fields'; import { Job, Datafeed, Detector } from './configs'; import { createBasicDetector } from './util/default_configs'; -import { JOB_TYPE, CREATED_BY_LABEL } from './util/constants'; +import { JOB_TYPE, CREATED_BY_LABEL } from '../../../../../../common/constants/new_job'; import { getRichDetectors } from './util/general'; export class PopulationJobCreator extends JobCreator { @@ -25,7 +25,11 @@ export class PopulationJobCreator extends JobCreator { private _byFields: SplitField[] = []; protected _type: JOB_TYPE = JOB_TYPE.POPULATION; - constructor(indexPattern: IndexPattern, savedSearch: SavedSearch, query: object) { + constructor( + indexPattern: IndexPattern, + savedSearch: SavedSearchSavedObject | null, + query: object + ) { super(indexPattern, savedSearch, query); this.createdBy = CREATED_BY_LABEL.POPULATION; } diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/single_metric_job_creator.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/single_metric_job_creator.ts index 5f3f6ff310d28..cb8a46ade513c 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/single_metric_job_creator.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/single_metric_job_creator.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedSearch } from 'src/legacy/core_plugins/kibana/public/discover/types'; import { IndexPattern } from 'ui/index_patterns'; +import { SavedSearchSavedObject } from '../../../../../../common/types/kibana'; import { parseInterval } from '../../../../../../common/util/parse_interval'; import { JobCreator } from './job_creator'; import { Field, Aggregation, AggFieldPair } from '../../../../../../common/types/fields'; @@ -15,13 +15,17 @@ import { ML_JOB_AGGREGATION, ES_AGGREGATION, } from '../../../../../../common/constants/aggregation_types'; -import { JOB_TYPE, CREATED_BY_LABEL } from './util/constants'; +import { JOB_TYPE, CREATED_BY_LABEL } from '../../../../../../common/constants/new_job'; import { getRichDetectors } from './util/general'; export class SingleMetricJobCreator extends JobCreator { protected _type: JOB_TYPE = JOB_TYPE.SINGLE_METRIC; - constructor(indexPattern: IndexPattern, savedSearch: SavedSearch, query: object) { + constructor( + indexPattern: IndexPattern, + savedSearch: SavedSearchSavedObject | null, + query: object + ) { super(indexPattern, savedSearch, query); this.createdBy = CREATED_BY_LABEL.SINGLE_METRIC; } diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/type_guards.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/type_guards.ts index 7162ec65767f9..9feb0416dd267 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/type_guards.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/type_guards.ts @@ -8,7 +8,7 @@ import { SingleMetricJobCreator } from './single_metric_job_creator'; import { MultiMetricJobCreator } from './multi_metric_job_creator'; import { PopulationJobCreator } from './population_job_creator'; import { AdvancedJobCreator } from './advanced_job_creator'; -import { JOB_TYPE } from './util/constants'; +import { JOB_TYPE } from '../../../../../../common/constants/new_job'; export type JobCreatorType = | SingleMetricJobCreator diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/util/general.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/util/general.ts index e7e5e8aa64f7b..760dbe447dc89 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/util/general.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/util/general.ts @@ -19,7 +19,7 @@ import { } from '../../../../../../../common/types/fields'; import { mlJobService } from '../../../../../services/job_service'; import { JobCreatorType, isMultiMetricJobCreator, isPopulationJobCreator } from '../index'; -import { CREATED_BY_LABEL, JOB_TYPE } from './constants'; +import { CREATED_BY_LABEL, JOB_TYPE } from '../../../../../../../common/constants/new_job'; const getFieldByIdFactory = (scriptFields: Field[]) => (id: string) => { let field = newJobCapsService.getFieldById(id); diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/results_loader/results_loader.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/results_loader/results_loader.ts index 82808ef3d37ee..5048f44586a38 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/results_loader/results_loader.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/results_loader/results_loader.ts @@ -12,8 +12,8 @@ import { getSeverityType } from '../../../../../../common/util/anomaly_utils'; import { parseInterval } from '../../../../../../common/util/parse_interval'; import { ANOMALY_SEVERITY } from '../../../../../../common/constants/anomalies'; import { getScoresByRecord } from './searches'; -import { JOB_TYPE } from '../job_creator/util/constants'; import { ChartLoader } from '../chart_loader'; +import { JOB_TYPE } from '../../../../../../common/constants/new_job'; import { ES_AGGREGATION } from '../../../../../../common/constants/aggregation_types'; export interface Results { diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/index.ts deleted file mode 100644 index 945d22967a65d..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/index.ts +++ /dev/null @@ -1,14 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import './pages/new_job/route'; -import './pages/new_job/directive'; -import './pages/job_type/route'; -import './pages/job_type/directive'; -import './pages/index_or_search/route'; -import './pages/index_or_search/directive'; -import './recognize/route'; -import './recognize/directive'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/query_delay/query_delay_input.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/query_delay/query_delay_input.tsx index 097050fd829c9..0e6dd81fb91a9 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/query_delay/query_delay_input.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/query_delay/query_delay_input.tsx @@ -9,7 +9,7 @@ import { EuiFieldText } from '@elastic/eui'; import { JobCreatorContext } from '../../../job_creator_context'; import { Description } from './description'; import { useStringifiedValue } from '../hooks'; -import { DEFAULT_QUERY_DELAY } from '../../../../../common/job_creator/util/constants'; +import { DEFAULT_QUERY_DELAY } from '../../../../../../../../../common/constants/new_job'; export const QueryDelayInput: FC = () => { const { jobCreator, jobCreatorUpdate, jobValidator, jobValidatorUpdated } = useContext( diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/advanced_section.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/advanced_section.tsx index 87e133df225a0..5fbc7557a2fa7 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/advanced_section.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/advanced_section.tsx @@ -17,7 +17,7 @@ import { ModelPlotSwitch } from './components/model_plot'; import { DedicatedIndexSwitch } from './components/dedicated_index'; import { ModelMemoryLimitInput } from '../../../common/model_memory_limit'; import { JobCreatorContext } from '../../../job_creator_context'; -import { JOB_TYPE } from '../../../../../common/job_creator/util/constants'; +import { JOB_TYPE } from '../../../../../../../../../common/constants/new_job'; const ButtonContent = i18n.translate( 'xpack.ml.newJob.wizard.jobDetailsStep.advancedSectionButton', diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/chart_grid.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/chart_grid.tsx index b76fc120538f5..f11cdc6233717 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/chart_grid.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/chart_grid.tsx @@ -11,7 +11,7 @@ import { AggFieldPair, SplitField } from '../../../../../../../../../common/type import { ChartSettings } from '../../../charts/common/settings'; import { LineChartData } from '../../../../../common/chart_loader'; import { ModelItem, Anomaly } from '../../../../../common/results_loader'; -import { JOB_TYPE } from '../../../../../common/job_creator/util/constants'; +import { JOB_TYPE } from '../../../../../../../../../common/constants/new_job'; import { SplitCards, useAnimateSplit } from '../split_cards'; import { DetectorTitle } from '../detector_title'; import { AnomalyChart, CHART_TYPE } from '../../../charts/anomaly_chart'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/chart_grid.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/chart_grid.tsx index 8cd533f8b2e29..035e3c90f53ae 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/chart_grid.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/chart_grid.tsx @@ -11,7 +11,7 @@ import { AggFieldPair, SplitField } from '../../../../../../../../../common/type import { ChartSettings } from '../../../charts/common/settings'; import { LineChartData } from '../../../../../common/chart_loader'; import { ModelItem, Anomaly } from '../../../../../common/results_loader'; -import { JOB_TYPE } from '../../../../../common/job_creator/util/constants'; +import { JOB_TYPE } from '../../../../../../../../../common/constants/new_job'; import { SplitCards, useAnimateSplit } from '../split_cards'; import { DetectorTitle } from '../detector_title'; import { ByFieldSelector } from '../split_field'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_cards/split_cards.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_cards/split_cards.tsx index 918163572076c..118923aa203e1 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_cards/split_cards.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_cards/split_cards.tsx @@ -9,7 +9,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiHorizontalRule, EuiSpacer } from '@elastic/eui'; import { SplitField } from '../../../../../../../../../common/types/fields'; -import { JOB_TYPE } from '../../../../../common/job_creator/util/constants'; +import { JOB_TYPE } from '../../../../../../../../../common/constants/new_job'; interface Props { fieldValues: string[]; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field/description.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field/description.tsx index 2c60130739780..6d4eeff2a5475 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field/description.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field/description.tsx @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiDescribedFormGroup, EuiFormRow } from '@elastic/eui'; -import { JOB_TYPE } from '../../../../../common/job_creator/util/constants'; +import { JOB_TYPE } from '../../../../../../../../../common/constants/new_job'; interface Props { jobType: JOB_TYPE; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/pick_fields.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/pick_fields.tsx index bdb2076086fd5..795dfc30f954a 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/pick_fields.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/pick_fields.tsx @@ -10,7 +10,7 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { JobCreatorContext } from '../job_creator_context'; import { WizardNav } from '../wizard_nav'; import { WIZARD_STEPS, StepProps } from '../step_types'; -import { JOB_TYPE } from '../../../common/job_creator/util/constants'; +import { JOB_TYPE } from '../../../../../../../common/constants/new_job'; import { SingleMetricView } from './components/single_metric_view'; import { MultiMetricView } from './components/multi_metric_view'; import { PopulationView } from './components/population_view'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/datafeed_details/datafeed_details.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/datafeed_details/datafeed_details.tsx index c624972aa07ea..d1900413d84c9 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/datafeed_details/datafeed_details.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/datafeed_details/datafeed_details.tsx @@ -10,7 +10,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiDescriptionList, EuiFormRow } from '@elas import { JobCreatorContext } from '../../../job_creator_context'; import { MLJobEditor } from '../../../../../../jobs_list/components/ml_job_editor'; import { calculateDatafeedFrequencyDefaultSeconds } from '../../../../../../../../../common/util/job_utils'; -import { DEFAULT_QUERY_DELAY } from '../../../../../common/job_creator/util/constants'; +import { DEFAULT_QUERY_DELAY } from '../../../../../../../../../common/constants/new_job'; import { getNewJobDefaults } from '../../../../../../../services/ml_server_info'; import { ListItems, defaultLabel, Italic } from '../common'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/detector_chart/detector_chart.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/detector_chart/detector_chart.tsx index bf60eda2e81c3..f72ff6cf985e5 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/detector_chart/detector_chart.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/detector_chart/detector_chart.tsx @@ -6,7 +6,7 @@ import React, { Fragment, FC, useContext } from 'react'; import { JobCreatorContext } from '../../../job_creator_context'; -import { JOB_TYPE } from '../../../../../common/job_creator/util/constants'; +import { JOB_TYPE } from '../../../../../../../../../common/constants/new_job'; import { SingleMetricView } from '../../../pick_fields_step/components/single_metric_view'; import { MultiMetricView } from '../../../pick_fields_step/components/multi_metric_view'; import { PopulationView } from '../../../pick_fields_step/components/population_view'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/summary.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/summary.tsx index 3f241f21a75e5..994847864d6bb 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/summary.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/summary.tsx @@ -23,7 +23,7 @@ import { JobRunner } from '../../../common/job_runner'; import { mlJobService } from '../../../../../services/job_service'; import { JsonEditorFlyout, EDITOR_MODE } from '../common/json_editor_flyout'; import { DatafeedPreviewFlyout } from '../common/datafeed_preview_flyout'; -import { JOB_TYPE } from '../../../common/job_creator/util/constants'; +import { JOB_TYPE } from '../../../../../../../common/constants/new_job'; import { isSingleMetricJobCreator, isAdvancedJobCreator } from '../../../common/job_creator'; import { JobDetails } from './components/job_details'; import { DatafeedDetails } from './components/datafeed_details'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/time_range_step/time_range.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/time_range_step/time_range.tsx index 7410e10aa92cf..70a529b8e24d0 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/time_range_step/time_range.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/time_range_step/time_range.tsx @@ -5,6 +5,8 @@ */ import React, { FC, Fragment, useContext, useEffect, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { toastNotifications } from 'ui/notify'; import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import { timefilter } from 'ui/timefilter'; @@ -16,7 +18,7 @@ import { useKibanaContext } from '../../../../../contexts/kibana'; import { FullTimeRangeSelector } from '../../../../../components/full_time_range_selector'; import { EventRateChart } from '../charts/event_rate_chart'; import { LineChartPoint } from '../../../common/chart_loader'; -import { JOB_TYPE } from '../../../common/job_creator/util/constants'; +import { JOB_TYPE } from '../../../../../../../common/constants/new_job'; import { GetTimeFieldRangeResponse } from '../../../../../services/ml_api_service'; import { TimeRangePicker, TimeRange } from '../../../common/components'; @@ -78,10 +80,18 @@ export const TimeRangeStep: FC = ({ setCurrentStep, isCurrentStep }) }, [jobCreatorUpdated]); function fullTimeRangeCallback(range: GetTimeFieldRangeResponse) { - setTimeRange({ - start: range.start.epoch, - end: range.end.epoch, - }); + if (range.start.epoch !== null && range.end.epoch !== null) { + setTimeRange({ + start: range.start.epoch, + end: range.end.epoch, + }); + } else { + toastNotifications.addDanger( + i18n.translate('xpack.ml.newJob.wizard.timeRangeStep.fullTimeRangeError', { + defaultMessage: 'An error occurred obtaining the time range for the index', + }) + ); + } } return ( diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/validation_step/validation.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/validation_step/validation.tsx index a543dbaaf3c5d..19b89ffec02ac 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/validation_step/validation.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/validation_step/validation.tsx @@ -10,7 +10,7 @@ import { WIZARD_STEPS, StepProps } from '../step_types'; import { JobCreatorContext } from '../job_creator_context'; import { mlJobService } from '../../../../../services/job_service'; import { ValidateJob } from '../../../../../components/validate_job'; -import { JOB_TYPE } from '../../../common/job_creator/util/constants'; +import { JOB_TYPE } from '../../../../../../../common/constants/new_job'; const idFilterList = [ 'job_id_valid', diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/index_or_search/__test__/directive.js b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/index_or_search/__test__/directive.js deleted file mode 100644 index ffa16930e79f2..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/index_or_search/__test__/directive.js +++ /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; - * you may not use this file except in compliance with the Elastic License. - */ - -import ngMock from 'ng_mock'; -import expect from '@kbn/expect'; -import sinon from 'sinon'; - -// Import this way to be able to stub/mock functions later on in the tests using sinon. -import * as indexUtils from '../../../../../util/index_utils'; - -describe('ML - Index or Saved Search selection directive', () => { - let $scope; - let $compile; - let $element; - - beforeEach(ngMock.module('kibana')); - beforeEach(() => { - ngMock.inject(function ($injector) { - $compile = $injector.get('$compile'); - const $rootScope = $injector.get('$rootScope'); - $scope = $rootScope.$new(); - }); - }); - - afterEach(() => { - $scope.$destroy(); - }); - - it('Initialize Index or Saved Search selection directive', done => { - sinon.stub(indexUtils, 'timeBasedIndexCheck').callsFake(() => false); - ngMock.inject(function () { - expect(() => { - $element = $compile('')($scope); - }).to.not.throwError(); - - // directive has scope: false - const scope = $element.isolateScope(); - expect(scope).to.eql(undefined); - done(); - }); - }); -}); diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/index_or_search/directive.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/index_or_search/directive.tsx deleted file mode 100644 index 9bd653708d9c0..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/index_or_search/directive.tsx +++ /dev/null @@ -1,42 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import ReactDOM from 'react-dom'; - -// @ts-ignore -import { uiModules } from 'ui/modules'; -const module = uiModules.get('apps/ml', ['react']); -import { timefilter } from 'ui/timefilter'; - -import { I18nContext } from 'ui/i18n'; -import { InjectorService } from '../../../../../../common/types/angular'; -import { Page } from './page'; - -module.directive('mlIndexOrSearch', ($injector: InjectorService) => { - return { - scope: {}, - restrict: 'E', - link: async (scope: ng.IScope, element: ng.IAugmentedJQuery) => { - // remove time picker from top of page - timefilter.disableTimeRangeSelector(); - timefilter.disableAutoRefreshSelector(); - - const $route = $injector.get('$route'); - const { nextStepPath } = $route.current.locals; - - ReactDOM.render( - {React.createElement(Page, { nextStepPath })}, - element[0] - ); - - element.on('$destroy', () => { - ReactDOM.unmountComponentAtNode(element[0]); - scope.$destroy(); - }); - }, - }; -}); diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/index_or_search/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/index_or_search/index.ts new file mode 100644 index 0000000000000..31e0f67c0a0fa --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/index_or_search/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { Page } from './page'; +export { preConfiguredJobRedirect } from './preconfigured_job_redirect'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/index_or_search/preconfigured_job_redirect.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/index_or_search/preconfigured_job_redirect.ts index 0265129d9ccab..8500279e742b7 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/index_or_search/preconfigured_job_redirect.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/index_or_search/preconfigured_job_redirect.ts @@ -4,16 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ +import { IndexPatternsContract } from '../../../../../../../../../../src/plugins/data/public'; import { mlJobService } from '../../../../services/job_service'; import { loadIndexPatterns, getIndexPatternIdFromName } from '../../../../util/index_utils'; import { CombinedJob } from '../../common/job_creator/configs'; -import { CREATED_BY_LABEL, JOB_TYPE } from '../../common/job_creator/util/constants'; +import { CREATED_BY_LABEL, JOB_TYPE } from '../../../../../../common/constants/new_job'; -export async function preConfiguredJobRedirect() { +export async function preConfiguredJobRedirect(indexPatterns: IndexPatternsContract) { const { job } = mlJobService.tempJobCloningObjects; if (job) { try { - await loadIndexPatterns(); + await loadIndexPatterns(indexPatterns); const redirectUrl = getWizardUrlFromCloningJob(job); window.location.href = `#/${redirectUrl}`; return Promise.reject(); diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/index_or_search/route.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/index_or_search/route.ts deleted file mode 100644 index 6dd5df177bd14..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/index_or_search/route.ts +++ /dev/null @@ -1,47 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import uiRoutes from 'ui/routes'; -import { checkMlNodesAvailable } from '../../../../ml_nodes_check'; -import { preConfiguredJobRedirect } from './preconfigured_job_redirect'; -import { checkLicenseExpired, checkBasicLicense } from '../../../../license/check_license'; -import { loadIndexPatterns } from '../../../../util/index_utils'; -import { - checkCreateJobsPrivilege, - checkFindFileStructurePrivilege, -} from '../../../../privilege/check_privilege'; -import { - getCreateJobBreadcrumbs, - getDataVisualizerIndexOrSearchBreadcrumbs, -} from '../../../breadcrumbs'; - -uiRoutes.when('/jobs/new_job', { - redirectTo: '/jobs/new_job/step/index_or_search', -}); - -uiRoutes.when('/jobs/new_job/step/index_or_search', { - template: '', - k7Breadcrumbs: getCreateJobBreadcrumbs, - resolve: { - CheckLicense: checkLicenseExpired, - privileges: checkCreateJobsPrivilege, - indexPatterns: loadIndexPatterns, - preConfiguredJobRedirect, - checkMlNodesAvailable, - nextStepPath: () => '#/jobs/new_job/step/job_type', - }, -}); - -uiRoutes.when('/datavisualizer_index_select', { - template: '', - k7Breadcrumbs: getDataVisualizerIndexOrSearchBreadcrumbs, - resolve: { - CheckLicense: checkBasicLicense, - privileges: checkFindFileStructurePrivilege, - indexPatterns: loadIndexPatterns, - nextStepPath: () => '#jobs/new_job/datavisualizer', - }, -}); diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/job_type/__test__/directive.js b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/job_type/__test__/directive.js deleted file mode 100644 index bdf65e3bafe96..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/job_type/__test__/directive.js +++ /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; - * you may not use this file except in compliance with the Elastic License. - */ - -import ngMock from 'ng_mock'; -import expect from '@kbn/expect'; -import sinon from 'sinon'; - -// Import this way to be able to stub/mock functions later on in the tests using sinon. -import * as indexUtils from '../../../../../util/index_utils'; - -describe('ML - Job Type Directive', () => { - let $scope; - let $compile; - let $element; - - beforeEach(ngMock.module('kibana')); - beforeEach(() => { - ngMock.inject(function ($injector) { - $compile = $injector.get('$compile'); - const $rootScope = $injector.get('$rootScope'); - $scope = $rootScope.$new(); - }); - }); - - afterEach(() => { - $scope.$destroy(); - }); - - it('Initialize Job Type Directive', done => { - sinon.stub(indexUtils, 'timeBasedIndexCheck').callsFake(() => false); - ngMock.inject(function () { - expect(() => { - $element = $compile('')($scope); - }).to.not.throwError(); - - // directive has scope: false - const scope = $element.isolateScope(); - expect(scope).to.eql(undefined); - done(); - }); - }); -}); diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/job_type/directive.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/job_type/directive.tsx deleted file mode 100644 index 3f2a7e553c7e0..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/job_type/directive.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; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import ReactDOM from 'react-dom'; - -// @ts-ignore -import { uiModules } from 'ui/modules'; -const module = uiModules.get('apps/ml', ['react']); -import { timefilter } from 'ui/timefilter'; - -import { I18nContext } from 'ui/i18n'; -import { InjectorService } from '../../../../../../common/types/angular'; -import { createSearchItems } from '../../utils/new_job_utils'; -import { Page } from './page'; - -import { KibanaContext, KibanaConfigTypeFix } from '../../../../contexts/kibana'; -import { IndexPatternsContract } from '../../../../../../../../../../src/plugins/data/public'; - -module.directive('mlJobTypePage', ($injector: InjectorService) => { - return { - scope: {}, - restrict: 'E', - link: async (scope: ng.IScope, element: ng.IAugmentedJQuery) => { - // remove time picker from top of page - timefilter.disableTimeRangeSelector(); - timefilter.disableAutoRefreshSelector(); - - const indexPatterns = $injector.get('indexPatterns'); - const kibanaConfig = $injector.get('config'); - const $route = $injector.get('$route'); - - const { indexPattern, savedSearch, combinedQuery } = createSearchItems( - kibanaConfig, - $route.current.locals.indexPattern, - $route.current.locals.savedSearch - ); - const kibanaContext = { - combinedQuery, - currentIndexPattern: indexPattern, - currentSavedSearch: savedSearch, - indexPatterns, - kibanaConfig, - }; - - ReactDOM.render( - - - {React.createElement(Page)} - - , - element[0] - ); - - element.on('$destroy', () => { - ReactDOM.unmountComponentAtNode(element[0]); - scope.$destroy(); - }); - }, - }; -}); diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/job_type/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/job_type/index.ts new file mode 100644 index 0000000000000..7e2d651439ae3 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/job_type/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { Page } from './page'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/job_type/page.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/job_type/page.tsx index 4991039ffa288..dbae1948cbe0f 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/job_type/page.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/job_type/page.tsx @@ -19,6 +19,7 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { useKibanaContext } from '../../../../contexts/kibana'; +import { isSavedSearchSavedObject } from '../../../../../../common/types/kibana'; import { DataRecognizer } from '../../../../components/data_recognizer'; import { addItemToRecentlyAccessed } from '../../../../util/recently_accessed'; import { timeBasedIndexCheck } from '../../../../util/index_utils'; @@ -32,33 +33,33 @@ export const Page: FC = () => { const isTimeBasedIndex = timeBasedIndexCheck(currentIndexPattern); const indexWarningTitle = - !isTimeBasedIndex && currentSavedSearch.id === undefined - ? i18n.translate('xpack.ml.newJob.wizard.jobType.indexPatternNotTimeBasedMessage', { - defaultMessage: 'Index pattern {indexPatternTitle} is not time based', - values: { indexPatternTitle: currentIndexPattern.title }, - }) - : i18n.translate( + !isTimeBasedIndex && isSavedSearchSavedObject(currentSavedSearch) + ? i18n.translate( 'xpack.ml.newJob.wizard.jobType.indexPatternFromSavedSearchNotTimeBasedMessage', { defaultMessage: '{savedSearchTitle} uses index pattern {indexPatternTitle} which is not time based', values: { - savedSearchTitle: currentSavedSearch.title, + savedSearchTitle: currentSavedSearch.attributes.title as string, indexPatternTitle: currentIndexPattern.title, }, } - ); - const pageTitleLabel = - currentSavedSearch.id !== undefined - ? i18n.translate('xpack.ml.newJob.wizard.jobType.savedSearchPageTitleLabel', { - defaultMessage: 'saved search {savedSearchTitle}', - values: { savedSearchTitle: currentSavedSearch.title }, - }) - : i18n.translate('xpack.ml.newJob.wizard.jobType.indexPatternPageTitleLabel', { - defaultMessage: 'index pattern {indexPatternTitle}', + ) + : i18n.translate('xpack.ml.newJob.wizard.jobType.indexPatternNotTimeBasedMessage', { + defaultMessage: 'Index pattern {indexPatternTitle} is not time based', values: { indexPatternTitle: currentIndexPattern.title }, }); + const pageTitleLabel = isSavedSearchSavedObject(currentSavedSearch) + ? i18n.translate('xpack.ml.newJob.wizard.jobType.savedSearchPageTitleLabel', { + defaultMessage: 'saved search {savedSearchTitle}', + values: { savedSearchTitle: currentSavedSearch.attributes.title as string }, + }) + : i18n.translate('xpack.ml.newJob.wizard.jobType.indexPatternPageTitleLabel', { + defaultMessage: 'index pattern {indexPatternTitle}', + values: { indexPatternTitle: currentIndexPattern.title }, + }); + const recognizerResults = { count: 0, onChange() { @@ -67,14 +68,15 @@ export const Page: FC = () => { }; const getUrl = (basePath: string) => { - return currentSavedSearch.id === undefined + return !isSavedSearchSavedObject(currentSavedSearch) ? `${basePath}?index=${currentIndexPattern.id}` : `${basePath}?savedSearchId=${currentSavedSearch.id}`; }; const addSelectionToRecentlyAccessed = () => { - const title = - currentSavedSearch.id === undefined ? currentIndexPattern.title : currentSavedSearch.title; + const title = !isSavedSearchSavedObject(currentSavedSearch) + ? currentIndexPattern.title + : (currentSavedSearch.attributes.title as string); const url = getUrl(''); addItemToRecentlyAccessed('jobs/new_job/datavisualizer', title, url); diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/job_type/route.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/job_type/route.ts deleted file mode 100644 index ac2c838dbed31..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/job_type/route.ts +++ /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; - * you may not use this file except in compliance with the Elastic License. - */ - -import uiRoutes from 'ui/routes'; - -import { checkMlNodesAvailable } from '../../../../ml_nodes_check'; -import { checkLicenseExpired } from '../../../../license/check_license'; -import { checkCreateJobsPrivilege } from '../../../../privilege/check_privilege'; -import { loadCurrentIndexPattern, loadCurrentSavedSearch } from '../../../../util/index_utils'; -import { getCreateJobBreadcrumbs } from '../../../breadcrumbs'; - -uiRoutes.when('/jobs/new_job/step/job_type', { - template: '', - k7Breadcrumbs: getCreateJobBreadcrumbs, - resolve: { - CheckLicense: checkLicenseExpired, - privileges: checkCreateJobsPrivilege, - indexPattern: loadCurrentIndexPattern, - savedSearch: loadCurrentSavedSearch, - checkMlNodesAvailable, - }, -}); diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/directive.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/directive.tsx deleted file mode 100644 index d152dfc488ff8..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/directive.tsx +++ /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; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import ReactDOM from 'react-dom'; - -// @ts-ignore -import { uiModules } from 'ui/modules'; -const module = uiModules.get('apps/ml', ['react']); -import { timefilter } from 'ui/timefilter'; -import { I18nContext } from 'ui/i18n'; -import { IndexPatternsContract } from '../../../../../../../../../../src/plugins/data/public'; - -import { InjectorService } from '../../../../../../common/types/angular'; -import { createSearchItems } from '../../utils/new_job_utils'; -import { Page, PageProps } from './page'; -import { JOB_TYPE } from '../../common/job_creator/util/constants'; - -import { KibanaContext, KibanaConfigTypeFix } from '../../../../contexts/kibana'; - -module.directive('mlNewJobPage', ($injector: InjectorService) => { - return { - scope: {}, - restrict: 'E', - link: async (scope: ng.IScope, element: ng.IAugmentedJQuery) => { - timefilter.disableTimeRangeSelector(); - timefilter.disableAutoRefreshSelector(); - - const indexPatterns = $injector.get('indexPatterns'); - const kibanaConfig = $injector.get('config'); - const $route = $injector.get('$route'); - const existingJobsAndGroups = $route.current.locals.existingJobsAndGroups; - - if ($route.current.locals.jobType === undefined) { - return; - } - const jobType: JOB_TYPE = $route.current.locals.jobType; - - const { indexPattern, savedSearch, combinedQuery } = createSearchItems( - kibanaConfig, - $route.current.locals.indexPattern, - $route.current.locals.savedSearch - ); - - const kibanaContext = { - combinedQuery, - currentIndexPattern: indexPattern, - currentSavedSearch: savedSearch, - indexPatterns, - kibanaConfig, - }; - - const props: PageProps = { - existingJobsAndGroups, - jobType, - }; - - ReactDOM.render( - - - {React.createElement(Page, props)} - - , - element[0] - ); - - element.on('$destroy', () => { - ReactDOM.unmountComponentAtNode(element[0]); - scope.$destroy(); - }); - }, - }; -}); diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/index.ts new file mode 100644 index 0000000000000..7e2d651439ae3 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { Page } from './page'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/page.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/page.tsx index e086b2b8aad7f..79f98c1170ff8 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/page.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/page.tsx @@ -16,7 +16,7 @@ import { JOB_TYPE, DEFAULT_MODEL_MEMORY_LIMIT, DEFAULT_BUCKET_SPAN, -} from '../../common/job_creator/util/constants'; +} from '../../../../../../common/constants/new_job'; import { ChartLoader } from '../../common/chart_loader'; import { ResultsLoader } from '../../common/results_loader'; import { JobValidator } from '../../common/job_validator'; @@ -104,7 +104,7 @@ export const Page: FC = ({ existingJobsAndGroups, jobType }) => { jobCreator.modelPlot = true; } - if (kibanaContext.currentSavedSearch.id !== undefined) { + if (kibanaContext.currentSavedSearch !== null) { // Jobs created from saved searches cannot be cloned in the wizard as the // ML job config holds no reference to the saved search ID. jobCreator.createdBy = null; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/route.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/route.ts deleted file mode 100644 index a527d92342d4c..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/route.ts +++ /dev/null @@ -1,65 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import uiRoutes from 'ui/routes'; - -import { checkFullLicense } from '../../../../license/check_license'; -import { checkGetJobsPrivilege } from '../../../../privilege/check_privilege'; -import { loadCurrentIndexPattern, loadCurrentSavedSearch } from '../../../../util/index_utils'; - -import { - getCreateSingleMetricJobBreadcrumbs, - getCreateMultiMetricJobBreadcrumbs, - getCreatePopulationJobBreadcrumbs, - getAdvancedJobConfigurationBreadcrumbs, -} from '../../../breadcrumbs'; - -import { Route } from '../../../../../../common/types/kibana'; - -import { loadNewJobCapabilities } from '../../../../services/new_job_capabilities_service'; - -import { loadMlServerInfo } from '../../../../services/ml_server_info'; - -import { mlJobService } from '../../../../services/job_service'; -import { JOB_TYPE } from '../../common/job_creator/util/constants'; - -const template = ``; - -const routes: Route[] = [ - { - id: JOB_TYPE.SINGLE_METRIC, - k7Breadcrumbs: getCreateSingleMetricJobBreadcrumbs, - }, - { - id: JOB_TYPE.MULTI_METRIC, - k7Breadcrumbs: getCreateMultiMetricJobBreadcrumbs, - }, - { - id: JOB_TYPE.POPULATION, - k7Breadcrumbs: getCreatePopulationJobBreadcrumbs, - }, - { - id: JOB_TYPE.ADVANCED, - k7Breadcrumbs: getAdvancedJobConfigurationBreadcrumbs, - }, -]; - -routes.forEach((route: Route) => { - uiRoutes.when(`/jobs/new_job/${route.id}`, { - template, - k7Breadcrumbs: route.k7Breadcrumbs, - resolve: { - CheckLicense: checkFullLicense, - privileges: checkGetJobsPrivilege, - indexPattern: loadCurrentIndexPattern, - savedSearch: loadCurrentSavedSearch, - loadNewJobCapabilities, - loadMlServerInfo, - existingJobsAndGroups: mlJobService.getJobAndGroupIds, - jobType: () => route.id, - }, - }); -}); diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/wizard.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/wizard.tsx index 50b8650f99bb8..b63ada4bb535c 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/wizard.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/wizard.tsx @@ -20,7 +20,7 @@ import { JobValidator } from '../../common/job_validator'; import { newJobCapsService } from '../../../../services/new_job_capabilities_service'; import { WizardSteps } from './wizard_steps'; import { WizardHorizontalSteps } from './wizard_horizontal_steps'; -import { JOB_TYPE } from '../../common/job_creator/util/constants'; +import { JOB_TYPE } from '../../../../../../common/constants/new_job'; interface Props { jobCreator: JobCreatorType; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/wizard_horizontal_steps.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/wizard_horizontal_steps.tsx index b5369402230b7..18b199ca8983f 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/wizard_horizontal_steps.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/wizard_horizontal_steps.tsx @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; import { EuiStepsHorizontal } from '@elastic/eui'; import { WIZARD_STEPS } from '../components/step_types'; -import { JOB_TYPE } from '../../common/job_creator/util/constants'; +import { JOB_TYPE } from '../../../../../../common/constants/new_job'; interface Props { currentStep: WIZARD_STEPS; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/wizard_steps.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/wizard_steps.tsx index 0f4eae230acfd..8e81c05092c98 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/wizard_steps.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/wizard_steps.tsx @@ -34,10 +34,10 @@ export const WizardSteps: FC = ({ currentStep, setCurrentStep }) => { const [additionalExpanded, setAdditionalExpanded] = useState(false); function getSummaryStepTitle() { - if (kibanaContext.currentSavedSearch.id !== undefined) { + if (kibanaContext.currentSavedSearch !== null) { return i18n.translate('xpack.ml.newJob.wizard.stepComponentWrapper.summaryTitleSavedSearch', { defaultMessage: 'New job from saved search {title}', - values: { title: kibanaContext.currentSavedSearch.title }, + values: { title: kibanaContext.currentSavedSearch.attributes.title as string }, }); } else if (kibanaContext.currentIndexPattern.id !== undefined) { return i18n.translate( diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/__test__/directive.js b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/__test__/directive.js deleted file mode 100644 index d5d5ee4438e32..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/__test__/directive.js +++ /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; - * you may not use this file except in compliance with the Elastic License. - */ - -import ngMock from 'ng_mock'; -import expect from '@kbn/expect'; -import sinon from 'sinon'; - -// Import this way to be able to stub/mock functions later on in the tests using sinon. -import * as indexUtils from '../../../../util/index_utils'; - -describe('ML - Recognize job directive', () => { - let $scope; - let $compile; - let $element; - - beforeEach(ngMock.module('kibana')); - beforeEach(() => { - ngMock.inject(function ($injector) { - $compile = $injector.get('$compile'); - const $rootScope = $injector.get('$rootScope'); - $scope = $rootScope.$new(); - }); - }); - - afterEach(() => { - $scope.$destroy(); - }); - - it('Initialize Recognize job directive', done => { - sinon.stub(indexUtils, 'timeBasedIndexCheck').callsFake(() => false); - ngMock.inject(function () { - expect(() => { - $element = $compile('')($scope); - }).to.not.throwError(); - - // directive has scope: false - const scope = $element.isolateScope(); - expect(scope).to.eql(undefined); - done(); - }); - }); -}); diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/directive.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/directive.tsx deleted file mode 100644 index 4ed12dfff4c20..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/directive.tsx +++ /dev/null @@ -1,68 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import ReactDOM from 'react-dom'; - -// @ts-ignore -import { uiModules } from 'ui/modules'; -const module = uiModules.get('apps/ml', ['react']); -import { timefilter } from 'ui/timefilter'; - -import { I18nContext } from 'ui/i18n'; -import { InjectorService } from '../../../../../common/types/angular'; - -import { createSearchItems } from '../utils/new_job_utils'; -import { Page } from './page'; - -import { KibanaContext, KibanaConfigTypeFix } from '../../../contexts/kibana'; -import { IndexPatternsContract } from '../../../../../../../../../src/plugins/data/public'; - -module.directive('mlRecognizePage', ($injector: InjectorService) => { - return { - scope: {}, - restrict: 'E', - link: async (scope: ng.IScope, element: ng.IAugmentedJQuery) => { - // remove time picker from top of page - timefilter.disableTimeRangeSelector(); - timefilter.disableAutoRefreshSelector(); - - const indexPatterns = $injector.get('indexPatterns'); - const kibanaConfig = $injector.get('config'); - const $route = $injector.get('$route'); - - const moduleId = $route.current.params.id; - const existingGroupIds: string[] = $route.current.locals.existingJobsAndGroups.groupIds; - - const { indexPattern, savedSearch, combinedQuery } = createSearchItems( - kibanaConfig, - $route.current.locals.indexPattern, - $route.current.locals.savedSearch - ); - const kibanaContext = { - combinedQuery, - currentIndexPattern: indexPattern, - currentSavedSearch: savedSearch, - indexPatterns, - kibanaConfig, - }; - - ReactDOM.render( - - - {React.createElement(Page, { moduleId, existingGroupIds })} - - , - element[0] - ); - - element.on('$destroy', () => { - ReactDOM.unmountComponentAtNode(element[0]); - scope.$destroy(); - }); - }, - }; -}); diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/index.ts new file mode 100644 index 0000000000000..7e2d651439ae3 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { Page } from './page'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/page.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/page.tsx index 11b2a8f01342d..141ed5d1bbb8f 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/page.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/page.tsx @@ -85,17 +85,17 @@ export const Page: FC = ({ moduleId, existingGroupIds }) => { combinedQuery, } = useKibanaContext(); const pageTitle = - savedSearch.id !== undefined + savedSearch !== null ? i18n.translate('xpack.ml.newJob.recognize.savedSearchPageTitle', { defaultMessage: 'saved search {savedSearchTitle}', - values: { savedSearchTitle: savedSearch.title }, + values: { savedSearchTitle: savedSearch.attributes.title as string }, }) : i18n.translate('xpack.ml.newJob.recognize.indexPatternPageTitle', { defaultMessage: 'index pattern {indexPatternTitle}', values: { indexPatternTitle: indexPattern.title }, }); - const displayQueryWarning = savedSearch.id !== undefined; - const tempQuery = savedSearch.id === undefined ? undefined : combinedQuery; + const displayQueryWarning = savedSearch !== null; + const tempQuery = savedSearch === null ? undefined : combinedQuery; /** * Loads recognizer module configuration. diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/resolvers.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/resolvers.ts index d2ca22972c201..cb44210b970e7 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/resolvers.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/resolvers.ts @@ -16,25 +16,20 @@ import { KibanaObjects } from './page'; * Redirects to the Anomaly Explorer to view the jobs if they have been created, * or the recognizer job wizard for the module if not. */ -export function checkViewOrCreateJobs($route: any) { +export function checkViewOrCreateJobs(moduleId: string, indexPatternId: string): Promise { return new Promise((resolve, reject) => { - const moduleId = $route.current.params.id; - const indexPatternId = $route.current.params.index; - // Load the module, and check if the job(s) in the module have been created. // If so, load the jobs in the Anomaly Explorer. // Otherwise open the data recognizer wizard for the module. // Always want to call reject() so as not to load original page. ml.dataRecognizerModuleJobsExist({ moduleId }) .then((resp: any) => { - const basePath = `${chrome.getBasePath()}/app/`; - if (resp.jobsExist === true) { const resultsPageUrl = mlJobService.createResultsUrlForJobs(resp.jobs, 'explorer'); - window.location.href = `${basePath}${resultsPageUrl}`; + window.location.href = resultsPageUrl; reject(); } else { - window.location.href = `${basePath}ml#/jobs/new_job/recognize?id=${moduleId}&index=${indexPatternId}`; + window.location.href = `#/jobs/new_job/recognize?id=${moduleId}&index=${indexPatternId}`; reject(); } }) diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/route.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/route.ts deleted file mode 100644 index 7b1d71540c163..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/route.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import uiRoutes from 'ui/routes'; -import { checkMlNodesAvailable } from '../../..//ml_nodes_check/check_ml_nodes'; -import { checkLicenseExpired } from '../../..//license/check_license'; -import { getCreateRecognizerJobBreadcrumbs } from '../../breadcrumbs'; -import { checkCreateJobsPrivilege } from '../../../privilege/check_privilege'; -import { loadCurrentIndexPattern, loadCurrentSavedSearch } from '../../../util/index_utils'; -import { mlJobService } from '../../../services/job_service'; -import { checkViewOrCreateJobs } from './resolvers'; - -uiRoutes.when('/jobs/new_job/recognize', { - template: '', - k7Breadcrumbs: getCreateRecognizerJobBreadcrumbs, - resolve: { - CheckLicense: checkLicenseExpired, - privileges: checkCreateJobsPrivilege, - indexPattern: loadCurrentIndexPattern, - savedSearch: loadCurrentSavedSearch, - checkMlNodesAvailable, - existingJobsAndGroups: mlJobService.getJobAndGroupIds, - }, -}); - -uiRoutes.when('/modules/check_view_or_create', { - template: '', - resolve: { - checkViewOrCreateJobs, - }, -}); diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/utils/new_job_utils.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/utils/new_job_utils.ts index 455fac9b532d6..050387e6de263 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/utils/new_job_utils.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/utils/new_job_utils.ts @@ -5,24 +5,18 @@ */ import { IndexPattern } from 'ui/index_patterns'; -import { SavedSearch } from 'src/legacy/core_plugins/kibana/public/discover/types'; +import { esQuery, Query, esKuery } from '../../../../../../../../../src/plugins/data/public'; import { KibanaConfigTypeFix } from '../../../contexts/kibana'; -import { esQuery, Query, IIndexPattern } from '../../../../../../../../../src/plugins/data/public'; - -export interface SearchItems { - indexPattern: IIndexPattern; - savedSearch: SavedSearch; - query: any; - combinedQuery: any; -} +import { SEARCH_QUERY_LANGUAGE } from '../../../../../common/constants/search'; +import { SavedSearchSavedObject } from '../../../../../common/types/kibana'; +import { getQueryFromSavedSearch } from '../../../util/index_utils'; // Provider for creating the items used for searching and job creation. -// Uses the $route object to retrieve the indexPattern and savedSearch from the url export function createSearchItems( kibanaConfig: KibanaConfigTypeFix, indexPattern: IndexPattern, - savedSearch: SavedSearch + savedSearch: SavedSearchSavedObject | null ) { // query is only used by the data visualizer as it needs // a lucene query_string. @@ -43,22 +37,36 @@ export function createSearchItems( }, }; - if (indexPattern.id === undefined && savedSearch.id !== undefined) { - const searchSource = savedSearch.searchSource; - indexPattern = searchSource.getField('index')!; + if (savedSearch !== null) { + const data = getQueryFromSavedSearch(savedSearch); - query = searchSource.getField('query')!; - const fs = searchSource.getField('filter'); + query = data.query; + const filter = data.filter; - const filters = Array.isArray(fs) ? fs : []; + const filters = Array.isArray(filter) ? filter : []; - const esQueryConfigs = esQuery.getEsQueryConfig(kibanaConfig); - combinedQuery = esQuery.buildEsQuery(indexPattern, [query], filters, esQueryConfigs); + if (query.language === SEARCH_QUERY_LANGUAGE.KUERY) { + const ast = esKuery.fromKueryExpression(query.query); + if (query.query !== '') { + combinedQuery = esKuery.toElasticsearchQuery(ast, indexPattern); + } + const filterQuery = esQuery.buildQueryFromFilters(filters, indexPattern); + + if (combinedQuery.bool.filter === undefined) { + combinedQuery.bool.filter = []; + } + if (combinedQuery.bool.must_not === undefined) { + combinedQuery.bool.must_not = []; + } + combinedQuery.bool.filter = [...combinedQuery.bool.filter, ...filterQuery.filter]; + combinedQuery.bool.must_not = [...combinedQuery.bool.must_not, ...filterQuery.must_not]; + } else { + const esQueryConfigs = esQuery.getEsQueryConfig(kibanaConfig); + combinedQuery = esQuery.buildEsQuery(indexPattern, [query], filters, esQueryConfigs); + } } return { - indexPattern, - savedSearch, query, combinedQuery, }; diff --git a/x-pack/legacy/plugins/ml/public/application/overview/breadcrumbs.ts b/x-pack/legacy/plugins/ml/public/application/overview/breadcrumbs.ts deleted file mode 100644 index 9df503b462b6c..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/overview/breadcrumbs.ts +++ /dev/null @@ -1,23 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; -// @ts-ignore -import { ML_BREADCRUMB } from '../../breadcrumbs'; - -export function getOverviewBreadcrumbs() { - // Whilst top level nav menu with tabs remains, - // use root ML breadcrumb. - return [ - ML_BREADCRUMB, - { - text: i18n.translate('xpack.ml.overviewBreadcrumbs.overviewLabel', { - defaultMessage: 'Overview', - }), - href: '', - }, - ]; -} diff --git a/x-pack/legacy/plugins/ml/public/application/overview/directive.tsx b/x-pack/legacy/plugins/ml/public/application/overview/directive.tsx deleted file mode 100644 index bd3b653ccbb64..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/overview/directive.tsx +++ /dev/null @@ -1,38 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import ReactDOM from 'react-dom'; -import React from 'react'; -import { I18nContext } from 'ui/i18n'; -import { timefilter } from 'ui/timefilter'; -// @ts-ignore -import { uiModules } from 'ui/modules'; -const module = uiModules.get('apps/ml', ['react']); - -import { OverviewPage } from './overview_page'; - -module.directive('mlOverview', function() { - return { - scope: {}, - restrict: 'E', - link: async (scope: ng.IScope, element: ng.IAugmentedJQuery) => { - timefilter.disableTimeRangeSelector(); - timefilter.disableAutoRefreshSelector(); - - ReactDOM.render( - - - , - element[0] - ); - - element.on('$destroy', () => { - ReactDOM.unmountComponentAtNode(element[0]); - scope.$destroy(); - }); - }, - }; -}); diff --git a/x-pack/legacy/plugins/ml/public/application/overview/index.ts b/x-pack/legacy/plugins/ml/public/application/overview/index.ts index ac00eab1f2cdb..7d99bb1094015 100644 --- a/x-pack/legacy/plugins/ml/public/application/overview/index.ts +++ b/x-pack/legacy/plugins/ml/public/application/overview/index.ts @@ -4,5 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -import './route'; -import './directive'; +export { OverviewPage } from './overview_page'; diff --git a/x-pack/legacy/plugins/ml/public/breadcrumbs.ts b/x-pack/legacy/plugins/ml/public/application/routing/breadcrumbs.ts similarity index 64% rename from x-pack/legacy/plugins/ml/public/breadcrumbs.ts rename to x-pack/legacy/plugins/ml/public/application/routing/breadcrumbs.ts index ba4703d4818ff..6d8138d4bcd2c 100644 --- a/x-pack/legacy/plugins/ml/public/breadcrumbs.ts +++ b/x-pack/legacy/plugins/ml/public/application/routing/breadcrumbs.ts @@ -5,31 +5,32 @@ */ import { i18n } from '@kbn/i18n'; +import { ChromeBreadcrumb } from '../../../../../../../src/core/public'; -export const ML_BREADCRUMB = Object.freeze({ +export const ML_BREADCRUMB: ChromeBreadcrumb = Object.freeze({ text: i18n.translate('xpack.ml.machineLearningBreadcrumbLabel', { defaultMessage: 'Machine Learning', }), href: '#/', }); -export const SETTINGS = Object.freeze({ +export const SETTINGS: ChromeBreadcrumb = Object.freeze({ text: i18n.translate('xpack.ml.settingsBreadcrumbLabel', { defaultMessage: 'Settings', }), - href: '#/settings?', + href: '#/settings', }); -export const ANOMALY_DETECTION_BREADCRUMB = Object.freeze({ +export const ANOMALY_DETECTION_BREADCRUMB: ChromeBreadcrumb = Object.freeze({ text: i18n.translate('xpack.ml.anomalyDetectionBreadcrumbLabel', { defaultMessage: 'Anomaly Detection', }), - href: '#/jobs?', + href: '#/jobs', }); -export const DATA_VISUALIZER_BREADCRUMB = Object.freeze({ +export const DATA_VISUALIZER_BREADCRUMB: ChromeBreadcrumb = Object.freeze({ text: i18n.translate('xpack.ml.datavisualizerBreadcrumbLabel', { defaultMessage: 'Data Visualizer', }), - href: '#/datavisualizer?', + href: '#/datavisualizer', }); diff --git a/x-pack/legacy/plugins/ml/public/application/routing/index.ts b/x-pack/legacy/plugins/ml/public/application/routing/index.ts new file mode 100644 index 0000000000000..3ec5361568526 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/routing/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { MlRouter, MlRoute } from './router'; diff --git a/x-pack/legacy/plugins/ml/public/application/overview/route.ts b/x-pack/legacy/plugins/ml/public/application/routing/resolvers.ts similarity index 50% rename from x-pack/legacy/plugins/ml/public/application/overview/route.ts rename to x-pack/legacy/plugins/ml/public/application/routing/resolvers.ts index e737961e184fc..30c5fbc497afe 100644 --- a/x-pack/legacy/plugins/ml/public/application/overview/route.ts +++ b/x-pack/legacy/plugins/ml/public/application/routing/resolvers.ts @@ -4,23 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ -import uiRoutes from 'ui/routes'; -import { getMlNodeCount } from '../ml_nodes_check/check_ml_nodes'; +import { loadIndexPatterns, loadSavedSearches } from '../util/index_utils'; import { checkFullLicense } from '../license/check_license'; import { checkGetJobsPrivilege } from '../privilege/check_privilege'; +import { getMlNodeCount } from '../ml_nodes_check/check_ml_nodes'; import { loadMlServerInfo } from '../services/ml_server_info'; -import { getOverviewBreadcrumbs } from './breadcrumbs'; -import './directive'; - -const template = ``; +import { PageDependencies } from './router'; -uiRoutes.when('/overview/?', { - template, - k7Breadcrumbs: getOverviewBreadcrumbs, - resolve: { - CheckLicense: checkFullLicense, - privileges: checkGetJobsPrivilege, - mlNodeCount: getMlNodeCount, - loadMlServerInfo, - }, +export interface Resolvers { + [name: string]: () => Promise; +} +export interface ResolverResults { + [name: string]: any; +} +export const basicResolvers = (deps: PageDependencies): Resolvers => ({ + checkFullLicense, + getMlNodeCount, + loadMlServerInfo, + loadIndexPatterns: () => loadIndexPatterns(deps.indexPatterns), + checkGetJobsPrivilege, + loadSavedSearches, }); diff --git a/x-pack/legacy/plugins/ml/public/application/routing/router.tsx b/x-pack/legacy/plugins/ml/public/application/routing/router.tsx new file mode 100644 index 0000000000000..174c1ef1d4fe8 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/routing/router.tsx @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { HashRouter, Route, RouteProps } from 'react-router-dom'; +import { Location } from 'history'; +import { I18nContext } from 'ui/i18n'; + +import { IndexPatternsContract } from '../../../../../../../src/plugins/data/public'; +import { KibanaContext, KibanaConfigTypeFix, KibanaContextValue } from '../contexts/kibana'; +import { ChromeBreadcrumb } from '../../../../../../../src/core/public'; + +import * as routes from './routes'; + +// custom RouteProps making location non-optional +interface MlRouteProps extends RouteProps { + location: Location; +} + +export interface MlRoute { + path: string; + render(props: MlRouteProps, config: KibanaConfigTypeFix, deps: PageDependencies): JSX.Element; + breadcrumbs: ChromeBreadcrumb[]; +} + +export interface PageProps { + location: Location; + config: KibanaConfigTypeFix; + deps: PageDependencies; +} + +export interface PageDependencies { + indexPatterns: IndexPatternsContract; +} + +export const PageLoader: FC<{ context: KibanaContextValue }> = ({ context, children }) => { + return context === null ? null : ( + + {children} + + ); +}; + +export const MlRouter: FC<{ + config: KibanaConfigTypeFix; + setBreadcrumbs: (breadcrumbs: ChromeBreadcrumb[]) => void; + indexPatterns: IndexPatternsContract; +}> = ({ config, setBreadcrumbs, indexPatterns }) => { + return ( + +
+ {Object.entries(routes).map(([name, route]) => ( + { + window.setTimeout(() => { + setBreadcrumbs(route.breadcrumbs); + }); + return route.render(props, config, { indexPatterns }); + }} + /> + ))} +
+
+ ); +}; diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/access_denied.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/access_denied.tsx new file mode 100644 index 0000000000000..3a2f445ac6b82 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/access_denied.tsx @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { i18n } from '@kbn/i18n'; +import { MlRoute, PageLoader, PageProps } from '../router'; +import { useResolver } from '../use_resolver'; +import { Page } from '../../access_denied'; + +const breadcrumbs = [ + { + text: i18n.translate('xpack.ml.accessDeniedLabel', { + defaultMessage: 'Access denied', + }), + href: '', + }, +]; + +export const accessDeniedRoute: MlRoute = { + path: '/access-denied', + render: (props, config, deps) => , + breadcrumbs, +}; + +const PageWrapper: FC = ({ config }) => { + const { context } = useResolver(undefined, undefined, config, {}); + + return ( + + + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx new file mode 100644 index 0000000000000..41c286c54836c --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { i18n } from '@kbn/i18n'; +import { decode } from 'rison-node'; + +// @ts-ignore +import queryString from 'query-string'; +import { MlRoute, PageLoader, PageProps } from '../../router'; +import { useResolver } from '../../use_resolver'; +import { basicResolvers } from '../../resolvers'; +import { Page } from '../../../data_frame_analytics/pages/analytics_exploration'; +import { ANALYSIS_CONFIG_TYPE } from '../../../data_frame_analytics/common/analytics'; +import { DATA_FRAME_TASK_STATE } from '../../../data_frame_analytics/pages/analytics_management/components/analytics_list/common'; +import { ML_BREADCRUMB } from '../../breadcrumbs'; + +const breadcrumbs = [ + ML_BREADCRUMB, + { + text: i18n.translate('xpack.ml.dataFrameAnalyticsBreadcrumbs.dataFrameExplorationLabel', { + defaultMessage: 'Data Frame Analytics', + }), + href: '', + }, +]; + +export const analyticsJobExplorationRoute: MlRoute = { + path: '/data_frame_analytics/exploration', + render: (props, config, deps) => , + breadcrumbs, +}; + +const PageWrapper: FC = ({ location, config, deps }) => { + const { context } = useResolver('', undefined, config, basicResolvers(deps)); + const { _g } = queryString.parse(location.search); + let globalState: any = null; + try { + globalState = decode(_g); + } catch (error) { + // eslint-disable-next-line no-console + console.error('Could not parse global state'); + window.location.href = '#data_frame_analytics'; + } + const jobId: string = globalState.ml.jobId; + const analysisType: ANALYSIS_CONFIG_TYPE = globalState.ml.analysisType; + const jobStatus: DATA_FRAME_TASK_STATE = globalState.ml.jobStatus; + + return ( + + + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_jobs_list.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_jobs_list.tsx new file mode 100644 index 0000000000000..31bd10f2138ad --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_jobs_list.tsx @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { i18n } from '@kbn/i18n'; + +import { MlRoute, PageLoader, PageProps } from '../../router'; +import { useResolver } from '../../use_resolver'; +import { basicResolvers } from '../../resolvers'; +import { Page } from '../../../data_frame_analytics/pages/analytics_management'; +import { ML_BREADCRUMB } from '../../breadcrumbs'; + +const breadcrumbs = [ + ML_BREADCRUMB, + { + text: i18n.translate('xpack.ml.dataFrameAnalyticsBreadcrumbs.dataFrameListLabel', { + defaultMessage: 'Data Frame Analytics', + }), + href: '', + }, +]; + +export const analyticsJobsListRoute: MlRoute = { + path: '/data_frame_analytics', + render: (props, config, deps) => , + breadcrumbs, +}; + +const PageWrapper: FC = ({ location, config, deps }) => { + const { context } = useResolver('', undefined, config, basicResolvers(deps)); + return ( + + + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/data_frame_analytics/index.ts b/x-pack/legacy/plugins/ml/public/application/routing/routes/data_frame_analytics/index.ts new file mode 100644 index 0000000000000..552c15a408b65 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/data_frame_analytics/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './analytics_jobs_list'; +export * from './analytics_job_exploration'; diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/datavisualizer/datavisualizer.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/datavisualizer/datavisualizer.tsx new file mode 100644 index 0000000000000..3faca285319d5 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/datavisualizer/datavisualizer.tsx @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; + +import { MlRoute, PageLoader, PageProps } from '../../router'; +import { useResolver } from '../../use_resolver'; +import { DatavisualizerSelector } from '../../../datavisualizer'; + +import { checkBasicLicense } from '../../../license/check_license'; +import { checkFindFileStructurePrivilege } from '../../../privilege/check_privilege'; +import { DATA_VISUALIZER_BREADCRUMB, ML_BREADCRUMB } from '../../breadcrumbs'; + +const breadcrumbs = [ML_BREADCRUMB, DATA_VISUALIZER_BREADCRUMB]; + +export const selectorRoute: MlRoute = { + path: '/datavisualizer', + render: (props, config, deps) => , + breadcrumbs, +}; + +const PageWrapper: FC = ({ location, config }) => { + const { context } = useResolver(undefined, undefined, config, { + checkBasicLicense, + checkFindFileStructurePrivilege, + }); + return ( + + + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/datavisualizer/file_based.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/datavisualizer/file_based.tsx new file mode 100644 index 0000000000000..11e6b85f939d3 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/datavisualizer/file_based.tsx @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { i18n } from '@kbn/i18n'; + +import { MlRoute, PageLoader, PageProps } from '../../router'; +import { useResolver } from '../../use_resolver'; +import { FileDataVisualizerPage } from '../../../datavisualizer/file_based'; + +import { checkBasicLicense } from '../../../license/check_license'; +import { checkFindFileStructurePrivilege } from '../../../privilege/check_privilege'; +import { loadIndexPatterns } from '../../../util/index_utils'; + +import { getMlNodeCount } from '../../../ml_nodes_check'; +import { DATA_VISUALIZER_BREADCRUMB, ML_BREADCRUMB } from '../../breadcrumbs'; + +const breadcrumbs = [ + ML_BREADCRUMB, + DATA_VISUALIZER_BREADCRUMB, + { + text: i18n.translate('xpack.ml.dataVisualizer.fileBasedLabel', { + defaultMessage: 'File', + }), + href: '', + }, +]; + +export const fileBasedRoute: MlRoute = { + path: '/filedatavisualizer', + render: (props, config, deps) => , + breadcrumbs, +}; + +const PageWrapper: FC = ({ location, config, deps }) => { + const { context } = useResolver('', undefined, config, { + checkBasicLicense, + loadIndexPatterns: () => loadIndexPatterns(deps.indexPatterns), + checkFindFileStructurePrivilege, + getMlNodeCount, + }); + return ( + + + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/datavisualizer/index.ts b/x-pack/legacy/plugins/ml/public/application/routing/routes/datavisualizer/index.ts new file mode 100644 index 0000000000000..7f61317ef3402 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/datavisualizer/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './datavisualizer'; +export * from './index_based'; +export * from './file_based'; diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx new file mode 100644 index 0000000000000..ab359238695d4 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { i18n } from '@kbn/i18n'; + +// @ts-ignore +import queryString from 'query-string'; +import { MlRoute, PageLoader, PageProps } from '../../router'; +import { useResolver } from '../../use_resolver'; +import { Page } from '../../../datavisualizer/index_based'; + +import { checkBasicLicense } from '../../../license/check_license'; +import { checkGetJobsPrivilege } from '../../../privilege/check_privilege'; +import { loadIndexPatterns } from '../../../util/index_utils'; +import { checkMlNodesAvailable } from '../../../ml_nodes_check'; +import { DATA_VISUALIZER_BREADCRUMB, ML_BREADCRUMB } from '../../breadcrumbs'; + +const breadcrumbs = [ + ML_BREADCRUMB, + DATA_VISUALIZER_BREADCRUMB, + { + text: i18n.translate('xpack.ml.dataFrameAnalyticsBreadcrumbs.indexLabel', { + defaultMessage: 'Index', + }), + href: '', + }, +]; + +export const indexBasedRoute: MlRoute = { + path: '/jobs/new_job/datavisualizer', + render: (props, config, deps) => , + breadcrumbs, +}; + +const PageWrapper: FC = ({ location, config, deps }) => { + const { index, savedSearchId } = queryString.parse(location.search); + const { context } = useResolver(index, savedSearchId, config, { + checkBasicLicense, + loadIndexPatterns: () => loadIndexPatterns(deps.indexPatterns), + checkGetJobsPrivilege, + checkMlNodesAvailable, + }); + + return ( + + + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/explorer.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/explorer.tsx new file mode 100644 index 0000000000000..1b6b91026d6a5 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/explorer.tsx @@ -0,0 +1,156 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, useEffect } from 'react'; +import { i18n } from '@kbn/i18n'; +import { decode } from 'rison-node'; +import { Subscription } from 'rxjs'; + +// @ts-ignore +import queryString from 'query-string'; +import { timefilter } from 'ui/timefilter'; +import { MlRoute, PageLoader, PageProps } from '../router'; +import { useResolver } from '../use_resolver'; +import { basicResolvers } from '../resolvers'; +import { Explorer } from '../../explorer'; +import { mlJobService } from '../../services/job_service'; +import { getExplorerDefaultAppState, ExplorerAppState } from '../../explorer/reducers'; +import { explorerService } from '../../explorer/explorer_dashboard_service'; +import { jobSelectServiceFactory } from '../../components/job_selector/job_select_service_utils'; +import { subscribeAppStateToObservable } from '../../util/app_state_utils'; + +import { interval$ } from '../../components/controls/select_interval'; +import { severity$ } from '../../components/controls/select_severity'; +import { showCharts$ } from '../../components/controls/checkbox_showcharts'; +import { ANOMALY_DETECTION_BREADCRUMB, ML_BREADCRUMB } from '../breadcrumbs'; + +const breadcrumbs = [ + ML_BREADCRUMB, + ANOMALY_DETECTION_BREADCRUMB, + { + text: i18n.translate('xpack.ml.anomalyDetection.anomalyExplorerLabel', { + defaultMessage: 'Anomaly Explorer', + }), + href: '', + }, +]; + +export const explorerRoute: MlRoute = { + path: '/explorer', + render: (props, config, deps) => , + breadcrumbs, +}; + +const PageWrapper: FC = ({ location, config, deps }) => { + const { index } = queryString.parse(location.search); + const { context } = useResolver(index, undefined, config, { + ...basicResolvers(deps), + jobs: mlJobService.loadJobsWrapper, + }); + const { _a, _g } = queryString.parse(location.search); + let appState: any = {}; + let globalState: any = {}; + try { + appState = decode(_a); + globalState = decode(_g); + } catch (error) { + // eslint-disable-next-line no-console + console.error('Could not parse global or app state'); + } + + if (appState.mlExplorerSwimlane === undefined) { + appState.mlExplorerSwimlane = {}; + } + + if (appState.mlExplorerFilter === undefined) { + appState.mlExplorerFilter = {}; + } + + appState.fetch = () => {}; + appState.on = () => {}; + appState.off = () => {}; + appState.save = () => {}; + globalState.fetch = () => {}; + globalState.on = () => {}; + globalState.off = () => {}; + globalState.save = () => {}; + + return ( + + + + ); +}; + +class AppState { + fetch() {} + on() {} + off() {} + save() {} +} + +const ExplorerWrapper: FC<{ globalState: any; appState: any }> = ({ globalState, appState }) => { + const subscriptions = new Subscription(); + + const { jobSelectService$, unsubscribeFromGlobalState } = jobSelectServiceFactory(globalState); + appState = getExplorerDefaultAppState(); + const { mlExplorerFilter, mlExplorerSwimlane } = appState; + window.setTimeout(() => { + // Pass the current URL AppState on to anomaly explorer's reactive state. + // After this hand-off, the appState stored in explorerState$ is the single + // source of truth. + explorerService.setAppState({ mlExplorerSwimlane, mlExplorerFilter }); + + // Now that appState in explorerState$ is the single source of truth, + // subscribe to it and update the actual URL appState on changes. + subscriptions.add( + explorerService.appState$.subscribe((appStateIn: ExplorerAppState) => { + // appState.fetch(); + appState.mlExplorerFilter = appStateIn.mlExplorerFilter; + appState.mlExplorerSwimlane = appStateIn.mlExplorerSwimlane; + // appState.save(); + }) + ); + }); + + subscriptions.add(subscribeAppStateToObservable(AppState, 'mlShowCharts', showCharts$, () => {})); + subscriptions.add( + subscribeAppStateToObservable(AppState, 'mlSelectInterval', interval$, () => {}) + ); + subscriptions.add( + subscribeAppStateToObservable(AppState, 'mlSelectSeverity', severity$, () => {}) + ); + + if (globalState.time) { + timefilter.setTime({ + from: globalState.time.from, + to: globalState.time.to, + }); + } + + useEffect(() => { + return () => { + subscriptions.unsubscribe(); + unsubscribeFromGlobalState(); + }; + }); + + return ( +
+ +
+ ); +}; diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/index.ts b/x-pack/legacy/plugins/ml/public/application/routing/routes/index.ts new file mode 100644 index 0000000000000..89ed35d5588f2 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './overview'; +export * from './jobs_list'; +export * from './new_job'; +export * from './datavisualizer'; +export * from './settings'; +export * from './data_frame_analytics'; +export * from './timeseriesexplorer'; +export * from './explorer'; +export * from './access_denied'; diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/jobs_list.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/jobs_list.tsx new file mode 100644 index 0000000000000..e61c24426bde9 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/jobs_list.tsx @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { i18n } from '@kbn/i18n'; +import { MlRoute, PageLoader, PageProps } from '../router'; +import { useResolver } from '../use_resolver'; +import { basicResolvers } from '../resolvers'; +import { JobsPage } from '../../jobs/jobs_list'; +import { ANOMALY_DETECTION_BREADCRUMB, ML_BREADCRUMB } from '../breadcrumbs'; + +const breadcrumbs = [ + ML_BREADCRUMB, + ANOMALY_DETECTION_BREADCRUMB, + { + text: i18n.translate('xpack.ml.anomalyDetection.jobManagementLabel', { + defaultMessage: 'Job Management', + }), + href: '', + }, +]; + +export const jobListRoute: MlRoute = { + path: '/jobs', + render: (props, config, deps) => , + breadcrumbs, +}; + +const PageWrapper: FC = ({ config, deps }) => { + const { context } = useResolver(undefined, undefined, config, basicResolvers(deps)); + + return ( + + + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/index.ts b/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/index.ts new file mode 100644 index 0000000000000..b226b75743ca6 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './index_or_search'; +export * from './job_type'; +export * from './new_job'; +export * from './wizard'; +export * from './recognize'; diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx new file mode 100644 index 0000000000000..b81058a9c89af --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { i18n } from '@kbn/i18n'; +import { MlRoute, PageLoader, PageDependencies } from '../../router'; +import { useResolver } from '../../use_resolver'; +import { basicResolvers } from '../../resolvers'; +import { Page, preConfiguredJobRedirect } from '../../../jobs/new_job/pages/index_or_search'; +import { ANOMALY_DETECTION_BREADCRUMB, ML_BREADCRUMB } from '../../breadcrumbs'; +import { KibanaConfigTypeFix } from '../../../contexts/kibana'; +import { checkBasicLicense } from '../../../license/check_license'; +import { loadIndexPatterns } from '../../../util/index_utils'; +import { checkGetJobsPrivilege } from '../../../privilege/check_privilege'; +import { checkMlNodesAvailable } from '../../../ml_nodes_check'; + +enum MODE { + NEW_JOB, + DATAVISUALIZER, +} + +const breadcrumbs = [ + ML_BREADCRUMB, + ANOMALY_DETECTION_BREADCRUMB, + { + text: i18n.translate('xpack.ml.jobsBreadcrumbs.selectIndexOrSearchLabel', { + defaultMessage: 'Create job', + }), + href: '', + }, +]; + +export const indexOrSearchRoute: MlRoute = { + path: '/jobs/new_job/step/index_or_search', + render: (props, config, deps) => ( + + ), + breadcrumbs, +}; + +export const dataVizIndexOrSearchRoute: MlRoute = { + path: '/datavisualizer_index_select', + render: (props, config, deps) => ( + + ), + breadcrumbs, +}; + +const PageWrapper: FC<{ + config: KibanaConfigTypeFix; + nextStepPath: string; + deps: PageDependencies; + mode: MODE; +}> = ({ config, nextStepPath, deps, mode }) => { + const newJobResolvers = { + ...basicResolvers(deps), + preConfiguredJobRedirect: () => preConfiguredJobRedirect(deps.indexPatterns), + }; + const dataVizResolvers = { + checkBasicLicense, + loadIndexPatterns: () => loadIndexPatterns(deps.indexPatterns), + checkGetJobsPrivilege, + checkMlNodesAvailable, + }; + + const { context } = useResolver( + undefined, + undefined, + config, + mode === MODE.NEW_JOB ? newJobResolvers : dataVizResolvers + ); + return ( + + + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/job_type.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/job_type.tsx new file mode 100644 index 0000000000000..e537a186ec784 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/job_type.tsx @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { i18n } from '@kbn/i18n'; + +// @ts-ignore +import queryString from 'query-string'; +import { MlRoute, PageLoader, PageProps } from '../../router'; +import { useResolver } from '../../use_resolver'; +import { basicResolvers } from '../../resolvers'; +import { Page } from '../../../jobs/new_job/pages/job_type'; +import { ANOMALY_DETECTION_BREADCRUMB, ML_BREADCRUMB } from '../../breadcrumbs'; + +const breadcrumbs = [ + ML_BREADCRUMB, + ANOMALY_DETECTION_BREADCRUMB, + { + text: i18n.translate('xpack.ml.jobsBreadcrumbs.selectJobType', { + defaultMessage: 'Create job', + }), + href: '', + }, +]; + +export const jobTypeRoute: MlRoute = { + path: '/jobs/new_job/step/job_type', + render: (props, config, deps) => , + breadcrumbs, +}; + +const PageWrapper: FC = ({ location, config, deps }) => { + const { index, savedSearchId } = queryString.parse(location.search); + const { context } = useResolver(index, savedSearchId, config, basicResolvers(deps)); + return ( + + + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/new_job.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/new_job.tsx new file mode 100644 index 0000000000000..b110434f6f0a8 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/new_job.tsx @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { i18n } from '@kbn/i18n'; +import { Redirect } from 'react-router-dom'; + +import { MlRoute } from '../../router'; +import { ANOMALY_DETECTION_BREADCRUMB, ML_BREADCRUMB } from '../../breadcrumbs'; + +const breadcrumbs = [ + ML_BREADCRUMB, + ANOMALY_DETECTION_BREADCRUMB, + { + text: i18n.translate('xpack.ml.jobsBreadcrumbs.jobWizardLabel', { + defaultMessage: 'Create job', + }), + href: '#/jobs/new_job', + }, +]; + +export const newJobRoute: MlRoute = { + path: '/jobs/new_job', + render: () => , + breadcrumbs, +}; + +const Page: FC = () => { + return ; +}; diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/recognize.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/recognize.tsx new file mode 100644 index 0000000000000..4f5085facfb29 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/recognize.tsx @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { i18n } from '@kbn/i18n'; +// @ts-ignore +import queryString from 'query-string'; + +import { MlRoute, PageLoader, PageProps } from '../../router'; +import { useResolver } from '../../use_resolver'; +import { basicResolvers } from '../../resolvers'; +import { Page } from '../../../jobs/new_job/recognize'; +import { checkViewOrCreateJobs } from '../../../jobs/new_job/recognize/resolvers'; +import { mlJobService } from '../../../services/job_service'; +import { ANOMALY_DETECTION_BREADCRUMB, ML_BREADCRUMB } from '../../breadcrumbs'; + +const breadcrumbs = [ + ML_BREADCRUMB, + ANOMALY_DETECTION_BREADCRUMB, + { + text: i18n.translate('xpack.ml.jobsBreadcrumbs.selectIndexOrSearchLabelRecognize', { + defaultMessage: 'Select index or search', + }), + href: '', + }, +]; + +export const recognizeRoute: MlRoute = { + path: '/jobs/new_job/recognize', + render: (props, config, deps) => , + breadcrumbs, +}; + +export const checkViewOrCreateRoute: MlRoute = { + path: '/modules/check_view_or_create', + render: (props, config, deps) => ( + + ), + breadcrumbs: [], +}; + +const PageWrapper: FC = ({ location, config, deps }) => { + const { id, index, savedSearchId } = queryString.parse(location.search); + const { context, results } = useResolver(index, savedSearchId, config, { + ...basicResolvers(deps), + existingJobsAndGroups: mlJobService.getJobAndGroupIds, + }); + + return ( + + + + ); +}; + +const CheckViewOrCreateWrapper: FC = ({ location, config, deps }) => { + const { id: moduleId, index: indexPatternId } = queryString.parse(location.search); + // the single resolver checkViewOrCreateJobs redirects only. so will always reject + useResolver(undefined, undefined, config, { + checkViewOrCreateJobs: () => checkViewOrCreateJobs(moduleId, indexPatternId), + }); + return null; +}; diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/wizard.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/wizard.tsx new file mode 100644 index 0000000000000..ea1baefdce0d1 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/wizard.tsx @@ -0,0 +1,121 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { i18n } from '@kbn/i18n'; +// @ts-ignore +import queryString from 'query-string'; + +import { basicResolvers } from '../../resolvers'; +import { MlRoute, PageLoader, PageProps } from '../../router'; +import { useResolver } from '../../use_resolver'; +import { Page } from '../../../jobs/new_job/pages/new_job'; +import { JOB_TYPE } from '../../../../../common/constants/new_job'; +import { mlJobService } from '../../../services/job_service'; +import { loadNewJobCapabilities } from '../../../services/new_job_capabilities_service'; +import { checkCreateJobsPrivilege } from '../../../privilege/check_privilege'; +import { ANOMALY_DETECTION_BREADCRUMB, ML_BREADCRUMB } from '../../breadcrumbs'; + +interface WizardPageProps extends PageProps { + jobType: JOB_TYPE; +} + +const createJobBreadcrumbs = { + text: i18n.translate('xpack.ml.jobsBreadcrumbs.createJobLabel', { + defaultMessage: 'Create job', + }), + href: '#/jobs/new_job', +}; + +const baseBreadcrumbs = [ML_BREADCRUMB, ANOMALY_DETECTION_BREADCRUMB, createJobBreadcrumbs]; + +const singleMetricBreadcrumbs = [ + ...baseBreadcrumbs, + { + text: i18n.translate('xpack.ml.jobsBreadcrumbs.singleMetricLabel', { + defaultMessage: 'Single metric', + }), + href: '', + }, +]; + +const multiMetricBreadcrumbs = [ + ...baseBreadcrumbs, + { + text: i18n.translate('xpack.ml.jobsBreadcrumbs.multiMetricLabel', { + defaultMessage: 'Multi metric', + }), + href: '', + }, +]; + +const populationBreadcrumbs = [ + ...baseBreadcrumbs, + { + text: i18n.translate('xpack.ml.jobsBreadcrumbs.populationLabel', { + defaultMessage: 'Population', + }), + href: '', + }, +]; + +const advancedBreadcrumbs = [ + ...baseBreadcrumbs, + { + text: i18n.translate('xpack.ml.jobsBreadcrumbs.advancedConfigurationLabel', { + defaultMessage: 'Advanced configuration', + }), + href: '', + }, +]; + +export const singleMetricRoute: MlRoute = { + path: '/jobs/new_job/single_metric', + render: (props, config, deps) => ( + + ), + breadcrumbs: singleMetricBreadcrumbs, +}; + +export const multiMetricRoute: MlRoute = { + path: '/jobs/new_job/multi_metric', + render: (props, config, deps) => ( + + ), + breadcrumbs: multiMetricBreadcrumbs, +}; + +export const populationRoute: MlRoute = { + path: '/jobs/new_job/population', + render: (props, config, deps) => ( + + ), + breadcrumbs: populationBreadcrumbs, +}; + +export const advancedRoute: MlRoute = { + path: '/jobs/new_job/advanced', + render: (props, config, deps) => ( + + ), + breadcrumbs: advancedBreadcrumbs, +}; + +const PageWrapper: FC = ({ location, config, jobType, deps }) => { + const { index, savedSearchId } = queryString.parse(location.search); + const { context, results } = useResolver(index, savedSearchId, config, { + ...basicResolvers(deps), + privileges: checkCreateJobsPrivilege, + jobCaps: () => loadNewJobCapabilities(index, savedSearchId, deps.indexPatterns), + existingJobsAndGroups: mlJobService.getJobAndGroupIds, + }); + + return ( + + + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/overview.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/overview.tsx new file mode 100644 index 0000000000000..fe9f4336148f3 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/overview.tsx @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { i18n } from '@kbn/i18n'; + +import { Redirect } from 'react-router-dom'; +import { MlRoute, PageLoader, PageProps } from '../router'; +import { useResolver } from '../use_resolver'; +import { OverviewPage } from '../../overview'; + +import { checkFullLicense } from '../../license/check_license'; +import { checkGetJobsPrivilege } from '../../privilege/check_privilege'; +import { getMlNodeCount } from '../../ml_nodes_check'; +import { loadMlServerInfo } from '../../services/ml_server_info'; +import { ML_BREADCRUMB } from '../breadcrumbs'; + +const breadcrumbs = [ + ML_BREADCRUMB, + { + text: i18n.translate('xpack.ml.overview.overviewLabel', { + defaultMessage: 'Overview', + }), + href: '#/overview', + }, +]; + +export const overviewRoute: MlRoute = { + path: '/overview', + render: (props, config, deps) => , + breadcrumbs, +}; + +const PageWrapper: FC = ({ config }) => { + const { context } = useResolver(undefined, undefined, config, { + checkFullLicense, + checkGetJobsPrivilege, + getMlNodeCount, + loadMlServerInfo, + }); + + return ( + + + + ); +}; + +export const appRootRoute: MlRoute = { + path: '/', + render: () => , + breadcrumbs: [], +}; + +const Page: FC = () => { + return ; +}; diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/calendar_list.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/calendar_list.tsx new file mode 100644 index 0000000000000..56ff57f6610b2 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/calendar_list.tsx @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { i18n } from '@kbn/i18n'; + +import { MlRoute, PageLoader, PageProps } from '../../router'; +import { useResolver } from '../../use_resolver'; + +import { checkFullLicense } from '../../../license/check_license'; +import { checkGetJobsPrivilege, checkPermission } from '../../../privilege/check_privilege'; +import { getMlNodeCount } from '../../../ml_nodes_check/check_ml_nodes'; +import { CalendarsList } from '../../../settings/calendars'; +import { SETTINGS, ML_BREADCRUMB } from '../../breadcrumbs'; + +const breadcrumbs = [ + ML_BREADCRUMB, + SETTINGS, + { + text: i18n.translate('xpack.ml.settings.breadcrumbs.calendarManagementLabel', { + defaultMessage: 'Calendar management', + }), + href: '#/settings/calendars_list', + }, +]; + +export const calendarListRoute: MlRoute = { + path: '/settings/calendars_list', + render: (props, config, deps) => , + breadcrumbs, +}; + +const PageWrapper: FC = ({ config }) => { + const { context } = useResolver(undefined, undefined, config, { + checkFullLicense, + checkGetJobsPrivilege, + getMlNodeCount, + }); + + const canCreateCalendar = checkPermission('canCreateCalendar'); + const canDeleteCalendar = checkPermission('canDeleteCalendar'); + + return ( + + + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/calendar_new_edit.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/calendar_new_edit.tsx new file mode 100644 index 0000000000000..fb68f103e1b77 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/calendar_new_edit.tsx @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { i18n } from '@kbn/i18n'; + +import { MlRoute, PageLoader, PageProps } from '../../router'; +import { useResolver } from '../../use_resolver'; + +import { checkFullLicense } from '../../../license/check_license'; +import { checkGetJobsPrivilege, checkPermission } from '../../../privilege/check_privilege'; +import { checkMlNodesAvailable } from '../../../ml_nodes_check/check_ml_nodes'; +import { NewCalendar } from '../../../settings/calendars'; +import { SETTINGS, ML_BREADCRUMB } from '../../breadcrumbs'; + +enum MODE { + NEW, + EDIT, +} + +interface NewCalendarPageProps extends PageProps { + mode: MODE; +} + +const newBreadcrumbs = [ + ML_BREADCRUMB, + SETTINGS, + { + text: i18n.translate('xpack.ml.settings.breadcrumbs.calendarManagement.createLabel', { + defaultMessage: 'Create', + }), + href: '#/settings/calendars_list/new_calendar', + }, +]; + +const editBreadcrumbs = [ + ML_BREADCRUMB, + SETTINGS, + { + text: i18n.translate('xpack.ml.settings.breadcrumbs.calendarManagement.editLabel', { + defaultMessage: 'Edit', + }), + href: '#/settings/calendars_list/edit_calendar', + }, +]; + +export const newCalendarRoute: MlRoute = { + path: '/settings/calendars_list/new_calendar', + render: (props, config, deps) => ( + + ), + breadcrumbs: newBreadcrumbs, +}; + +export const editCalendarRoute: MlRoute = { + path: '/settings/calendars_list/edit_calendar/:calendarId', + render: (props, config, deps) => ( + + ), + breadcrumbs: editBreadcrumbs, +}; + +const PageWrapper: FC = ({ location, config, mode }) => { + let calendarId: string | undefined; + if (mode === MODE.EDIT) { + const pathMatch: string[] | null = location.pathname.match(/.+\/(.+)$/); + calendarId = pathMatch && pathMatch.length > 1 ? pathMatch[1] : undefined; + } + + const { context } = useResolver(undefined, undefined, config, { + checkFullLicense, + checkGetJobsPrivilege, + checkMlNodesAvailable, + }); + + const canCreateCalendar = checkPermission('canCreateCalendar'); + const canDeleteCalendar = checkPermission('canDeleteCalendar'); + + return ( + + + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/filter_list.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/filter_list.tsx new file mode 100644 index 0000000000000..cb19883e962c1 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/filter_list.tsx @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { i18n } from '@kbn/i18n'; + +import { MlRoute, PageLoader, PageProps } from '../../router'; +import { useResolver } from '../../use_resolver'; + +import { checkFullLicense } from '../../../license/check_license'; +import { checkGetJobsPrivilege, checkPermission } from '../../../privilege/check_privilege'; +import { getMlNodeCount } from '../../../ml_nodes_check/check_ml_nodes'; +import { FilterLists } from '../../../settings/filter_lists'; + +import { SETTINGS, ML_BREADCRUMB } from '../../breadcrumbs'; + +const breadcrumbs = [ + ML_BREADCRUMB, + SETTINGS, + { + text: i18n.translate('xpack.ml.settings.breadcrumbs.filterListsLabel', { + defaultMessage: 'Filter lists', + }), + href: '#/settings/filter_lists', + }, +]; + +export const filterListRoute: MlRoute = { + path: '/settings/filter_lists', + render: (props, config, deps) => , + breadcrumbs, +}; + +const PageWrapper: FC = ({ config }) => { + const { context } = useResolver(undefined, undefined, config, { + checkFullLicense, + checkGetJobsPrivilege, + getMlNodeCount, + }); + + const canCreateFilter = checkPermission('canCreateFilter'); + const canDeleteFilter = checkPermission('canDeleteFilter'); + + return ( + + + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/filter_list_new_edit.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/filter_list_new_edit.tsx new file mode 100644 index 0000000000000..7a596a488ddb6 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/filter_list_new_edit.tsx @@ -0,0 +1,98 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { i18n } from '@kbn/i18n'; + +import { MlRoute, PageLoader, PageProps } from '../../router'; +import { useResolver } from '../../use_resolver'; + +import { checkFullLicense } from '../../../license/check_license'; +import { checkGetJobsPrivilege, checkPermission } from '../../../privilege/check_privilege'; +import { checkMlNodesAvailable } from '../../../ml_nodes_check/check_ml_nodes'; +import { EditFilterList } from '../../../settings/filter_lists'; +import { SETTINGS, ML_BREADCRUMB } from '../../breadcrumbs'; + +enum MODE { + NEW, + EDIT, +} + +interface NewFilterPageProps extends PageProps { + mode: MODE; +} + +const newBreadcrumbs = [ + ML_BREADCRUMB, + SETTINGS, + { + text: i18n.translate('xpack.ml.settings.breadcrumbs.filterLists.createLabel', { + defaultMessage: 'Create', + }), + href: '#/settings/filter_lists/new', + }, +]; + +const editBreadcrumbs = [ + ML_BREADCRUMB, + SETTINGS, + { + text: i18n.translate('xpack.ml.settings.breadcrumbs.filterLists.editLabel', { + defaultMessage: 'Edit', + }), + href: '#/settings/filter_lists/edit', + }, +]; + +export const newFilterListRoute: MlRoute = { + path: '/settings/filter_lists/new_filter_list', + render: (props, config, deps) => ( + + ), + breadcrumbs: newBreadcrumbs, +}; + +export const editFilterListRoute: MlRoute = { + path: '/settings/filter_lists/edit_filter_list/:filterId', + render: (props, config, deps) => ( + + ), + breadcrumbs: editBreadcrumbs, +}; + +const PageWrapper: FC = ({ location, config, mode }) => { + let filterId: string | undefined; + if (mode === MODE.EDIT) { + const pathMatch: string[] | null = location.pathname.match(/.+\/(.+)$/); + filterId = pathMatch && pathMatch.length > 1 ? pathMatch[1] : undefined; + } + + const { context } = useResolver(undefined, undefined, config, { + checkFullLicense, + checkGetJobsPrivilege, + checkMlNodesAvailable, + }); + + const canCreateFilter = checkPermission('canCreateFilter'); + const canDeleteFilter = checkPermission('canDeleteFilter'); + + return ( + + + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/index.ts b/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/index.ts new file mode 100644 index 0000000000000..f638b78e05fb1 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './settings'; +export * from './calendar_list'; +export * from './calendar_new_edit'; +export * from './filter_list'; +export * from './filter_list_new_edit'; diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/settings.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/settings.tsx new file mode 100644 index 0000000000000..b62ecc0539e72 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/settings.tsx @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; + +import { MlRoute, PageLoader, PageProps } from '../../router'; +import { useResolver } from '../../use_resolver'; + +import { checkFullLicense } from '../../../license/check_license'; +import { checkGetJobsPrivilege, checkPermission } from '../../../privilege/check_privilege'; +import { getMlNodeCount } from '../../../ml_nodes_check/check_ml_nodes'; +import { Settings } from '../../../settings'; +import { ML_BREADCRUMB, SETTINGS } from '../../breadcrumbs'; + +const breadcrumbs = [ML_BREADCRUMB, SETTINGS]; + +export const settingsRoute: MlRoute = { + path: '/settings', + render: (props, config, deps) => , + breadcrumbs, +}; + +const PageWrapper: FC = ({ config }) => { + const { context } = useResolver(undefined, undefined, config, { + checkFullLicense, + checkGetJobsPrivilege, + getMlNodeCount, + }); + + const canGetFilters = checkPermission('canGetFilters'); + const canGetCalendars = checkPermission('canGetCalendars'); + + return ( + + + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx new file mode 100644 index 0000000000000..a40bbfa214b28 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx @@ -0,0 +1,155 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, useEffect } from 'react'; +import { i18n } from '@kbn/i18n'; +import { decode } from 'rison-node'; +import moment from 'moment'; +import { Subscription } from 'rxjs'; + +// @ts-ignore +import queryString from 'query-string'; +import { timefilter } from 'ui/timefilter'; +import { MlRoute, PageLoader, PageProps } from '../router'; +import { useResolver } from '../use_resolver'; +import { basicResolvers } from '../resolvers'; +import { TimeSeriesExplorer } from '../../timeseriesexplorer'; +import { mlJobService } from '../../services/job_service'; +import { APP_STATE_ACTION } from '../../timeseriesexplorer/timeseriesexplorer_constants'; +import { subscribeAppStateToObservable } from '../../util/app_state_utils'; +import { interval$ } from '../../components/controls/select_interval'; +import { severity$ } from '../../components/controls/select_severity'; +import { ANOMALY_DETECTION_BREADCRUMB, ML_BREADCRUMB } from '../breadcrumbs'; + +export const timeSeriesExplorerRoute: MlRoute = { + path: '/timeseriesexplorer', + render: (props, config, deps) => , + breadcrumbs: [ + ML_BREADCRUMB, + ANOMALY_DETECTION_BREADCRUMB, + { + text: i18n.translate('xpack.ml.anomalyDetection.singleMetricViewerLabel', { + defaultMessage: 'Single Metric Viewer', + }), + href: '', + }, + ], +}; + +const PageWrapper: FC = ({ location, config, deps }) => { + const { context } = useResolver('', undefined, config, { + ...basicResolvers(deps), + jobs: mlJobService.loadJobsWrapper, + }); + const { _a, _g } = queryString.parse(location.search); + let appState: any = {}; + let globalState: any = {}; + try { + appState = decode(_a); + globalState = decode(_g); + } catch (error) { + // eslint-disable-next-line no-console + console.error('Could not parse global or app state'); + } + if (appState.mlTimeSeriesExplorer === undefined) { + appState.mlTimeSeriesExplorer = {}; + } + globalState.fetch = () => {}; + globalState.on = () => {}; + globalState.off = () => {}; + globalState.save = () => {}; + + return ( + + + + ); +}; + +class AppState { + fetch() {} + on() {} + off() {} + save() {} +} + +const TimeSeriesExplorerWrapper: FC<{ globalState: any; appState: any; config: any }> = ({ + globalState, + appState, + config, +}) => { + if (globalState.time) { + timefilter.setTime({ + from: globalState.time.from, + to: globalState.time.to, + }); + } + + const subscriptions = new Subscription(); + subscriptions.add( + subscribeAppStateToObservable(AppState, 'mlSelectInterval', interval$, () => {}) + ); + subscriptions.add( + subscribeAppStateToObservable(AppState, 'mlSelectSeverity', severity$, () => {}) + ); + + const appStateHandler = (action: string, payload: any) => { + switch (action) { + case APP_STATE_ACTION.CLEAR: + delete appState.mlTimeSeriesExplorer.detectorIndex; + delete appState.mlTimeSeriesExplorer.entities; + delete appState.mlTimeSeriesExplorer.forecastId; + break; + + case APP_STATE_ACTION.GET_DETECTOR_INDEX: + return appState.mlTimeSeriesExplorer.detectorIndex; + case APP_STATE_ACTION.SET_DETECTOR_INDEX: + appState.mlTimeSeriesExplorer.detectorIndex = payload; + break; + + case APP_STATE_ACTION.GET_ENTITIES: + return appState.mlTimeSeriesExplorer.entities; + case APP_STATE_ACTION.SET_ENTITIES: + appState.mlTimeSeriesExplorer.entities = payload; + break; + + case APP_STATE_ACTION.GET_FORECAST_ID: + return appState.mlTimeSeriesExplorer.forecastId; + case APP_STATE_ACTION.SET_FORECAST_ID: + appState.mlTimeSeriesExplorer.forecastId = payload; + break; + + case APP_STATE_ACTION.GET_ZOOM: + return appState.mlTimeSeriesExplorer.zoom; + case APP_STATE_ACTION.SET_ZOOM: + appState.mlTimeSeriesExplorer.zoom = payload; + break; + case APP_STATE_ACTION.UNSET_ZOOM: + delete appState.mlTimeSeriesExplorer.zoom; + break; + } + }; + + useEffect(() => { + return () => { + subscriptions.unsubscribe(); + }; + }); + + const tzConfig = config.get('dateFormat:tz'); + const dateFormatTz = tzConfig !== 'Browser' ? tzConfig : moment.tz.guess(); + + return ( + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/application/routing/use_resolver.ts b/x-pack/legacy/plugins/ml/public/application/routing/use_resolver.ts new file mode 100644 index 0000000000000..f74260c06567e --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/routing/use_resolver.ts @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useEffect, useState } from 'react'; +import { + getIndexPatternById, + getIndexPatternsContract, + getIndexPatternAndSavedSearch, +} from '../util/index_utils'; +import { createSearchItems } from '../jobs/new_job/utils/new_job_utils'; +import { ResolverResults, Resolvers } from './resolvers'; +import { KibanaConfigTypeFix, KibanaContextValue } from '../contexts/kibana'; + +export const useResolver = ( + indexPatternId: string | undefined, + savedSearchId: string | undefined, + config: KibanaConfigTypeFix, + resolvers: Resolvers +): { context: KibanaContextValue; results: ResolverResults } => { + const funcNames = Object.keys(resolvers); // Object.entries gets this wrong?! + const funcs = Object.values(resolvers); // Object.entries gets this wrong?! + const tempResults = funcNames.reduce((p, c) => { + p[c] = {}; + return p; + }, {} as ResolverResults); + + const [context, setContext] = useState(null); + const [results, setResults] = useState(tempResults); + + useEffect(() => { + (async () => { + try { + const res = await Promise.all(funcs.map(r => r())); + res.forEach((r, i) => (tempResults[funcNames[i]] = r)); + setResults(tempResults); + + if (indexPatternId !== undefined || savedSearchId !== undefined) { + // note, currently we're using our own kibana context that requires a current index pattern to be set + // this means, if the page uses this context, useResolver must be passed a string for the index pattern id + // and loadIndexPatterns must be part of the resolvers. + const { indexPattern, savedSearch } = + savedSearchId !== undefined + ? await getIndexPatternAndSavedSearch(savedSearchId) + : { savedSearch: null, indexPattern: await getIndexPatternById(indexPatternId!) }; + + const { combinedQuery } = createSearchItems(config, indexPattern!, savedSearch); + + setContext({ + combinedQuery, + currentIndexPattern: indexPattern, + currentSavedSearch: savedSearch, + indexPatterns: getIndexPatternsContract()!, + kibanaConfig: config, + }); + } else { + setContext({}); + } + } catch (error) { + // quietly fail. Let the resolvers handle the redirection if any fail to resolve + // eslint-disable-next-line no-console + console.error('ML page loading resolver', error); + } + })(); + }, []); + + return { context, results }; +}; diff --git a/x-pack/legacy/plugins/ml/public/application/services/job_service.d.ts b/x-pack/legacy/plugins/ml/public/application/services/job_service.d.ts index a3096a942a7c7..b9ed83eeffba1 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/job_service.d.ts +++ b/x-pack/legacy/plugins/ml/public/application/services/job_service.d.ts @@ -35,10 +35,10 @@ declare interface JobService { end: number | undefined ): Promise; createResultsUrl(jobId: string[], start: number, end: number, location: string): string; - getJobAndGroupIds(): ExistingJobsAndGroups; + getJobAndGroupIds(): Promise; searchPreview(job: CombinedJob): Promise>; getJob(jobId: string): CombinedJob; - loadJobsWrapper(): Promise; + loadJobsWrapper(): Promise; } export const mlJobService: JobService; diff --git a/x-pack/legacy/plugins/ml/public/application/services/job_service.js b/x-pack/legacy/plugins/ml/public/application/services/job_service.js index 90aa5d8d66faa..dbe81df0f0471 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/job_service.js +++ b/x-pack/legacy/plugins/ml/public/application/services/job_service.js @@ -912,7 +912,7 @@ function createResultsUrl(jobIds, start, end, resultsPage) { let path = ''; if (resultsPage !== undefined) { - path += 'ml#/'; + path += '#/'; path += resultsPage; } diff --git a/x-pack/legacy/plugins/ml/public/application/services/new_job_capabilities_service.ts b/x-pack/legacy/plugins/ml/public/application/services/new_job_capabilities_service.ts index f79515c80556a..9d5c33d6cfc5c 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/new_job_capabilities_service.ts +++ b/x-pack/legacy/plugins/ml/public/application/services/new_job_capabilities_service.ts @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedSearchLoader } from 'src/legacy/core_plugins/kibana/public/discover/types'; - import { Field, Aggregation, @@ -20,18 +18,16 @@ import { IndexPatternsContract, } from '../../../../../../../src/plugins/data/public'; import { ml } from './ml_api_service'; +import { getIndexPatternAndSavedSearch } from '../util/index_utils'; // called in the angular routing resolve block to initialize the // newJobCapsService with the currently selected index pattern export function loadNewJobCapabilities( - indexPatterns: IndexPatternsContract, - savedSearches: SavedSearchLoader, - $route: Record + indexPatternId: string, + savedSearchId: string, + indexPatterns: IndexPatternsContract ) { return new Promise(async (resolve, reject) => { - // get the index pattern id or saved search id from the url params - const { index: indexPatternId, savedSearchId } = $route.current.params; - if (indexPatternId !== undefined) { // index pattern is being used const indexPattern: IndexPattern = await indexPatterns.get(indexPatternId); @@ -40,8 +36,13 @@ export function loadNewJobCapabilities( } else if (savedSearchId !== undefined) { // saved search is being used // load the index pattern from the saved search - const savedSearch = await savedSearches.get(savedSearchId); - const indexPattern = savedSearch.searchSource.getField('index')!; + const { indexPattern } = await getIndexPatternAndSavedSearch(savedSearchId); + if (indexPattern === null) { + // eslint-disable-next-line no-console + console.error('Cannot retrieve index pattern from saved search'); + reject(); + return; + } await newJobCapsService.initializeFromIndexPattern(indexPattern); resolve(newJobCapsService.newJobCaps); } else { diff --git a/x-pack/legacy/plugins/ml/public/application/settings/breadcrumbs.ts b/x-pack/legacy/plugins/ml/public/application/settings/breadcrumbs.ts deleted file mode 100644 index bd04003c9eca4..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/settings/breadcrumbs.ts +++ /dev/null @@ -1,86 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; -import { ML_BREADCRUMB, ANOMALY_DETECTION_BREADCRUMB, SETTINGS } from '../../breadcrumbs'; - -export function getSettingsBreadcrumbs() { - // Whilst top level nav menu with tabs remains, - // use root ML breadcrumb. - return [ML_BREADCRUMB, ANOMALY_DETECTION_BREADCRUMB, SETTINGS]; -} - -export function getCalendarManagementBreadcrumbs() { - return [ - ...getSettingsBreadcrumbs(), - { - text: i18n.translate('xpack.ml.settings.breadcrumbs.calendarManagementLabel', { - defaultMessage: 'Calendar management', - }), - href: '#/settings/calendars_list', - }, - ]; -} - -export function getCreateCalendarBreadcrumbs() { - return [ - ...getCalendarManagementBreadcrumbs(), - { - text: i18n.translate('xpack.ml.settings.breadcrumbs.calendarManagement.createLabel', { - defaultMessage: 'Create', - }), - href: '#/settings/calendars_list/new_calendar', - }, - ]; -} - -export function getEditCalendarBreadcrumbs() { - return [ - ...getCalendarManagementBreadcrumbs(), - { - text: i18n.translate('xpack.ml.settings.breadcrumbs.calendarManagement.editLabel', { - defaultMessage: 'Edit', - }), - href: '#/settings/calendars_list/edit_calendar', - }, - ]; -} - -export function getFilterListsBreadcrumbs() { - return [ - ...getSettingsBreadcrumbs(), - { - text: i18n.translate('xpack.ml.settings.breadcrumbs.filterListsLabel', { - defaultMessage: 'Filter lists', - }), - href: '#/settings/filter_lists', - }, - ]; -} - -export function getCreateFilterListBreadcrumbs() { - return [ - ...getFilterListsBreadcrumbs(), - { - text: i18n.translate('xpack.ml.settings.breadcrumbs.filterLists.createLabel', { - defaultMessage: 'Create', - }), - href: '#/settings/filter_lists/new', - }, - ]; -} - -export function getEditFilterListBreadcrumbs() { - return [ - ...getFilterListsBreadcrumbs(), - { - text: i18n.translate('xpack.ml.settings.breadcrumbs.filterLists.editLabel', { - defaultMessage: 'Edit', - }), - href: '#/settings/filter_lists/edit', - }, - ]; -} diff --git a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/calendar_form/__snapshots__/calendar_form.test.js.snap b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/calendar_form/__snapshots__/calendar_form.test.js.snap index 408042fb70cba..267fb3930121b 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/calendar_form/__snapshots__/calendar_form.test.js.snap +++ b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/calendar_form/__snapshots__/calendar_form.test.js.snap @@ -181,7 +181,7 @@ exports[`CalendarForm Renders calendar form 1`] = ` grow={false} > - -`; - -uiRoutes - .when('/settings/calendars_list/new_calendar', { - template, - k7Breadcrumbs: getCreateCalendarBreadcrumbs, - resolve: { - CheckLicense: checkFullLicense, - privileges: checkGetJobsPrivilege, - checkMlNodesAvailable, - }, - }) - .when('/settings/calendars_list/edit_calendar/:calendarId', { - template, - k7Breadcrumbs: getEditCalendarBreadcrumbs, - resolve: { - CheckLicense: checkFullLicense, - privileges: checkGetJobsPrivilege, - checkMlNodesAvailable, - }, - }); - -module.directive('mlNewCalendar', function($route: any) { - return { - restrict: 'E', - replace: false, - scope: {}, - link(scope: ng.IScope, element: ng.IAugmentedJQuery) { - const props = { - calendarId: $route.current.params.calendarId, - canCreateCalendar: checkPermission('canCreateCalendar'), - canDeleteCalendar: checkPermission('canDeleteCalendar'), - }; - - ReactDOM.render( - - - , - element[0] - ); - }, - }; -}); diff --git a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/index.ts b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/index.ts index aa8b2ec2c29c9..5e008e4796d1c 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/index.ts +++ b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -import './directive'; +export { NewCalendar } from './new_calendar'; diff --git a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_calendar.d.ts b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_calendar.d.ts index d6de538d6388a..002a88ec03f0d 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_calendar.d.ts +++ b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_calendar.d.ts @@ -7,7 +7,7 @@ import { FC } from 'react'; declare const NewCalendar: FC<{ - calendarId: string; + calendarId?: string; canCreateCalendar: boolean; canDeleteCalendar: boolean; }>; diff --git a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_calendar.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_calendar.js index feabd60d8d3a0..c9fe2503b0c5b 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_calendar.js +++ b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_calendar.js @@ -18,7 +18,6 @@ import { EuiOverlayMask, } from '@elastic/eui'; -import chrome from 'ui/chrome'; import { toastNotifications } from 'ui/notify'; import { NavigationMenu } from '../../../components/navigation_menu'; @@ -153,7 +152,7 @@ export const NewCalendar = injectI18n(class NewCalendar extends Component { try { await ml.addCalendar(calendar); - window.location = `${chrome.getBasePath()}/app/ml#/settings/calendars_list`; + window.location = '#/settings/calendars_list'; } catch (error) { console.log('Error saving calendar', error); this.setState({ saving: false }); @@ -176,7 +175,7 @@ export const NewCalendar = injectI18n(class NewCalendar extends Component { try { await ml.updateCalendar(calendar); - window.location = `${chrome.getBasePath()}/app/ml#/settings/calendars_list`; + window.location = '#/settings/calendars_list'; } catch (error) { console.log('Error saving calendar', error); this.setState({ saving: false }); diff --git a/x-pack/legacy/plugins/ml/public/application/settings/calendars/index.ts b/x-pack/legacy/plugins/ml/public/application/settings/calendars/index.ts new file mode 100644 index 0000000000000..88aa20ea06320 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/settings/calendars/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { NewCalendar } from './edit'; +export { CalendarsList } from './list'; diff --git a/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/directive.tsx b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/directive.tsx deleted file mode 100644 index 1b90a27c07ada..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/directive.tsx +++ /dev/null @@ -1,58 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import 'ngreact'; -import React from 'react'; -import ReactDOM from 'react-dom'; - -// @ts-ignore -import { uiModules } from 'ui/modules'; -const module = uiModules.get('apps/ml', ['react']); - -import uiRoutes from 'ui/routes'; -import { I18nContext } from 'ui/i18n'; -import { checkFullLicense } from '../../../license/check_license'; -import { checkGetJobsPrivilege, checkPermission } from '../../../privilege/check_privilege'; -import { getMlNodeCount } from '../../../ml_nodes_check/check_ml_nodes'; -import { getCalendarManagementBreadcrumbs } from '../../breadcrumbs'; - -import { CalendarsList } from './calendars_list'; - -const template = ` -
- -`; - -uiRoutes.when('/settings/calendars_list', { - template, - k7Breadcrumbs: getCalendarManagementBreadcrumbs, - resolve: { - CheckLicense: checkFullLicense, - privileges: checkGetJobsPrivilege, - mlNodeCount: getMlNodeCount, - }, -}); - -module.directive('mlCalendarsList', function() { - return { - restrict: 'E', - replace: false, - scope: {}, - link(scope: ng.IScope, element: ng.IAugmentedJQuery) { - const props = { - canCreateCalendar: checkPermission('canCreateCalendar'), - canDeleteCalendar: checkPermission('canDeleteCalendar'), - }; - - ReactDOM.render( - - - , - element[0] - ); - }, - }; -}); diff --git a/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/index.ts b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/index.ts index aa8b2ec2c29c9..dcf8ade3301b3 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/index.ts +++ b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -import './directive'; +export { CalendarsList } from './calendars_list'; diff --git a/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/table/__snapshots__/table.test.js.snap b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/table/__snapshots__/table.test.js.snap index 1932ff3d83efa..7958546f3a0cf 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/table/__snapshots__/table.test.js.snap +++ b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/table/__snapshots__/table.test.js.snap @@ -70,7 +70,7 @@ exports[`CalendarsListTable renders the table with all calendars 1`] = ` "toolsRight": Array [ diff --git a/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/table/table.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/table/table.js index 3a5c8eec31c06..285f9423cd4c1 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/table/table.js +++ b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/table/table.js @@ -15,8 +15,6 @@ import { EuiInMemoryTable, } from '@elastic/eui'; -import chrome from 'ui/chrome'; - import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; @@ -55,7 +53,7 @@ export const CalendarsListTable = injectI18n(function CalendarsListTable({ truncateText: true, render: (id) => ( {id} @@ -98,7 +96,7 @@ export const CalendarsListTable = injectI18n(function CalendarsListTable({ size="s" data-test-subj="mlCalendarButtonCreate" key="new_calendar_button" - href={`${chrome.getBasePath()}/app/ml#/settings/calendars_list/new_calendar`} + href="#/settings/calendars_list/new_calendar" isDisabled={(canCreateCalendar === false || mlNodesAvailable === false)} > - -`; - -uiRoutes - .when('/settings/filter_lists/new_filter_list', { - template, - k7Breadcrumbs: getCreateFilterListBreadcrumbs, - resolve: { - CheckLicense: checkFullLicense, - privileges: checkGetJobsPrivilege, - mlNodeCount: getMlNodeCount, - }, - }) - .when('/settings/filter_lists/edit_filter_list/:filterId', { - template, - k7Breadcrumbs: getEditFilterListBreadcrumbs, - resolve: { - CheckLicense: checkFullLicense, - privileges: checkGetJobsPrivilege, - mlNodeCount: getMlNodeCount, - }, - }); - -module.directive('mlEditFilterList', function($route: any) { - return { - restrict: 'E', - replace: false, - scope: {}, - link(scope: ng.IScope, element: ng.IAugmentedJQuery) { - const props = { - filterId: $route.current.params.filterId, - canCreateFilter: checkPermission('canCreateFilter'), - canDeleteFilter: checkPermission('canDeleteFilter'), - }; - - ReactDOM.render( - - - , - element[0] - ); - }, - }; -}); diff --git a/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/edit_filter_list.d.ts b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/edit_filter_list.d.ts index 71d82d7694cf0..56ef8745cb28b 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/edit_filter_list.d.ts +++ b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/edit_filter_list.d.ts @@ -7,7 +7,7 @@ import { FC } from 'react'; declare const EditFilterList: FC<{ - filterId: string; + filterId?: string; canCreateFilter: boolean; canDeleteFilter: boolean; }>; diff --git a/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/index.ts b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/index.ts index aa8b2ec2c29c9..52b35f361777f 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/index.ts +++ b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -import './directive'; +export { EditFilterList } from './edit_filter_list'; diff --git a/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/index.ts b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/index.ts index 6a942d5c251df..52dcda9b3f7c0 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/index.ts +++ b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/index.ts @@ -4,5 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -import './edit'; -import './list'; +export { EditFilterList } from './edit'; +export { FilterLists } from './list'; diff --git a/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/directive.tsx b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/directive.tsx deleted file mode 100644 index 7b572344c603b..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/directive.tsx +++ /dev/null @@ -1,58 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import 'ngreact'; -import React from 'react'; -import ReactDOM from 'react-dom'; - -// @ts-ignore -import { uiModules } from 'ui/modules'; -const module = uiModules.get('apps/ml', ['react']); - -import uiRoutes from 'ui/routes'; -import { I18nContext } from 'ui/i18n'; -import { checkFullLicense } from '../../../license/check_license'; -import { checkGetJobsPrivilege, checkPermission } from '../../../privilege/check_privilege'; -import { getMlNodeCount } from '../../../ml_nodes_check/check_ml_nodes'; -import { getFilterListsBreadcrumbs } from '../../breadcrumbs'; - -import { FilterLists } from './filter_lists'; - -const template = ` -
- -`; - -uiRoutes.when('/settings/filter_lists', { - template, - k7Breadcrumbs: getFilterListsBreadcrumbs, - resolve: { - CheckLicense: checkFullLicense, - privileges: checkGetJobsPrivilege, - mlNodeCount: getMlNodeCount, - }, -}); - -module.directive('mlFilterLists', function() { - return { - restrict: 'E', - replace: false, - scope: {}, - link(scope: ng.IScope, element: ng.IAugmentedJQuery) { - const props = { - canCreateFilter: checkPermission('canCreateFilter'), - canDeleteFilter: checkPermission('canDeleteFilter'), - }; - - ReactDOM.render( - - - , - element[0] - ); - }, - }; -}); diff --git a/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/index.ts b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/index.ts index aa8b2ec2c29c9..2e5cc371e317d 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/index.ts +++ b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -import './directive'; +export { FilterLists } from './filter_lists'; diff --git a/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/table.js b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/table.js index a5cc1ed761b56..85bba8e4fe744 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/table.js +++ b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/table.js @@ -26,7 +26,6 @@ import { import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import chrome from 'ui/chrome'; import { DeleteFilterListModal } from '../components/delete_filter_list_modal'; @@ -68,7 +67,7 @@ function NewFilterButton({ canCreateFilter }) { return ( @@ -89,7 +88,7 @@ function getColumns() { defaultMessage: 'ID', }), render: (id) => ( - + {id} ), diff --git a/x-pack/legacy/plugins/ml/public/application/settings/index.ts b/x-pack/legacy/plugins/ml/public/application/settings/index.ts index d9fc996ae4a30..db74dcb1a1846 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/index.ts +++ b/x-pack/legacy/plugins/ml/public/application/settings/index.ts @@ -4,6 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -import './settings_directive'; -import './calendars'; -import './filter_lists'; +export { Settings } from './settings'; diff --git a/x-pack/legacy/plugins/ml/public/application/settings/settings.tsx b/x-pack/legacy/plugins/ml/public/application/settings/settings.tsx index 3c1ae2973721a..225f39fc6f419 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/settings.tsx +++ b/x-pack/legacy/plugins/ml/public/application/settings/settings.tsx @@ -19,7 +19,6 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; -import { useUiChromeContext } from '../contexts/ui/use_ui_chrome_context'; import { NavigationMenu } from '../components/navigation_menu'; interface Props { @@ -28,8 +27,6 @@ interface Props { } export const Settings: FC = ({ canGetFilters, canGetCalendars }) => { - const basePath = useUiChromeContext().getBasePath(); - return ( @@ -53,7 +50,7 @@ export const Settings: FC = ({ canGetFilters, canGetCalendars }) => { data-test-subj="ml_calendar_mng_button" size="l" color="primary" - href={`${basePath}/app/ml#/settings/calendars_list`} + href="#/settings/calendars_list" isDisabled={canGetCalendars === false} > = ({ canGetFilters, canGetCalendars }) => { data-test-subj="ml_filter_lists_button" size="l" color="primary" - href={`${basePath}/app/ml#/settings/filter_lists`} + href="#/settings/filter_lists" isDisabled={canGetFilters === false} > - -`; - -uiRoutes.when('/settings', { - template, - k7Breadcrumbs: getSettingsBreadcrumbs, - resolve: { - CheckLicense: checkFullLicense, - privileges: checkGetJobsPrivilege, - mlNodeCount: getMlNodeCount, - }, -}); - -import { Settings } from './settings'; - -module.directive('mlSettings', function() { - const canGetFilters = checkPermission('canGetFilters'); - const canGetCalendars = checkPermission('canGetCalendars'); - - return { - restrict: 'E', - replace: false, - scope: {}, - link(scope: ng.IScope, element: ng.IAugmentedJQuery) { - timefilter.disableTimeRangeSelector(); - timefilter.disableAutoRefreshSelector(); - - ReactDOM.render( - - - , - element[0] - ); - }, - }; -}); diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/breadcrumbs.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/breadcrumbs.js deleted file mode 100644 index 2aa4c845b125d..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/breadcrumbs.js +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - - -import { ML_BREADCRUMB, ANOMALY_DETECTION_BREADCRUMB } from '../../breadcrumbs'; -import { i18n } from '@kbn/i18n'; - - -export function getSingleMetricViewerBreadcrumbs() { - // Whilst top level nav menu with tabs remains, - // use root ML breadcrumb. - return [ - ML_BREADCRUMB, - ANOMALY_DETECTION_BREADCRUMB, - { - text: i18n.translate('xpack.ml.anomalyDetection.singleMetricViewerLabel', { - defaultMessage: 'Single Metric Viewer' - }), - href: '' - } - - ]; -} - diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/index.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/index.js deleted file mode 100644 index 5aa6cfe8835ad..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/index.js +++ /dev/null @@ -1,11 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import './timeseriesexplorer_directive'; -import './timeseriesexplorer_route'; -import './timeseries_search_service'; -import '../components/job_selector'; -import '../components/chart_tooltip'; diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/index.ts b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/index.ts new file mode 100644 index 0000000000000..6877e8ef754fd --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { TimeSeriesExplorer } from './timeseriesexplorer'; diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.d.ts b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.d.ts new file mode 100644 index 0000000000000..ac4bc6186e5b4 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.d.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Timefilter } from 'ui/timefilter'; +import { FC } from 'react'; + +declare const TimeSeriesExplorer: FC<{ + appStateHandler: (action: string, payload: any) => void; + dateFormatTz: string; + globalState: any; + timefilter: Timefilter; +}>; diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js index a70e1d38784e9..99eb4beb977da 100644 --- a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js @@ -142,7 +142,7 @@ const TimeSeriesExplorerPage = ({ children, jobSelectorProps, loading, resizeRef If we'd just show no progress bar when not loading it would result in a flickering height effect. */} {!loading && ()} -
+
{children}
diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_directive.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_directive.js deleted file mode 100644 index 048a8dbc4a6db..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_directive.js +++ /dev/null @@ -1,111 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { get } from 'lodash'; -import moment from 'moment-timezone'; -import { Subscription } from 'rxjs'; - -import React from 'react'; -import ReactDOM from 'react-dom'; - -import { uiModules } from 'ui/modules'; -const module = uiModules.get('apps/ml'); - -import { timefilter } from 'ui/timefilter'; -import { I18nContext } from 'ui/i18n'; - -import '../components/controls'; - -import { severity$ } from '../components/controls/select_severity/select_severity'; -import { interval$ } from '../components/controls/select_interval/select_interval'; -import { subscribeAppStateToObservable } from '../util/app_state_utils'; - -import { TimeSeriesExplorer } from './timeseriesexplorer'; -import { APP_STATE_ACTION } from './timeseriesexplorer_constants'; - -module.directive('mlTimeSeriesExplorer', function ($injector) { - function link($scope, $element) { - const globalState = $injector.get('globalState'); - const AppState = $injector.get('AppState'); - const config = $injector.get('config'); - - const subscriptions = new Subscription(); - subscriptions.add(subscribeAppStateToObservable(AppState, 'mlSelectInterval', interval$)); - subscriptions.add(subscribeAppStateToObservable(AppState, 'mlSelectSeverity', severity$)); - - $scope.appState = new AppState({ mlTimeSeriesExplorer: {} }); - - const appStateHandler = (action, payload) => { - $scope.appState.fetch(); - switch (action) { - case APP_STATE_ACTION.CLEAR: - delete $scope.appState.mlTimeSeriesExplorer.detectorIndex; - delete $scope.appState.mlTimeSeriesExplorer.entities; - delete $scope.appState.mlTimeSeriesExplorer.forecastId; - break; - - case APP_STATE_ACTION.GET_DETECTOR_INDEX: - return get($scope, 'appState.mlTimeSeriesExplorer.detectorIndex'); - case APP_STATE_ACTION.SET_DETECTOR_INDEX: - $scope.appState.mlTimeSeriesExplorer.detectorIndex = payload; - break; - - case APP_STATE_ACTION.GET_ENTITIES: - return get($scope, 'appState.mlTimeSeriesExplorer.entities', {}); - case APP_STATE_ACTION.SET_ENTITIES: - $scope.appState.mlTimeSeriesExplorer.entities = payload; - break; - - case APP_STATE_ACTION.GET_FORECAST_ID: - return get($scope, 'appState.mlTimeSeriesExplorer.forecastId'); - case APP_STATE_ACTION.SET_FORECAST_ID: - $scope.appState.mlTimeSeriesExplorer.forecastId = payload; - break; - - case APP_STATE_ACTION.GET_ZOOM: - return get($scope, 'appState.mlTimeSeriesExplorer.zoom'); - case APP_STATE_ACTION.SET_ZOOM: - $scope.appState.mlTimeSeriesExplorer.zoom = payload; - break; - case APP_STATE_ACTION.UNSET_ZOOM: - delete $scope.appState.mlTimeSeriesExplorer.zoom; - break; - } - $scope.appState.save(); - $scope.$applyAsync(); - }; - - function updateComponent() { - // Pass the timezone to the server for use when aggregating anomalies (by day / hour) for the table. - const tzConfig = config.get('dateFormat:tz'); - const dateFormatTz = (tzConfig !== 'Browser') ? tzConfig : moment.tz.guess(); - - ReactDOM.render( - - - , - $element[0] - ); - } - - $element.on('$destroy', () => { - ReactDOM.unmountComponentAtNode($element[0]); - subscriptions.unsubscribe(); - }); - - updateComponent(); - } - - return { - link, - }; -}); diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_route.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_route.js deleted file mode 100644 index 63b9b819be315..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_route.js +++ /dev/null @@ -1,30 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import uiRoutes from 'ui/routes'; - -import '../components/controls'; - -import { checkFullLicense } from '../license/check_license'; -import { getMlNodeCount } from '../ml_nodes_check/check_ml_nodes'; -import { checkGetJobsPrivilege } from '../privilege/check_privilege'; -import { mlJobService } from '../services/job_service'; -import { loadIndexPatterns } from '../util/index_utils'; - -import { getSingleMetricViewerBreadcrumbs } from './breadcrumbs'; - -uiRoutes - .when('/timeseriesexplorer/?', { - template: '', - k7Breadcrumbs: getSingleMetricViewerBreadcrumbs, - resolve: { - CheckLicense: checkFullLicense, - privileges: checkGetJobsPrivilege, - indexPatterns: loadIndexPatterns, - mlNodeCount: getMlNodeCount, - jobs: mlJobService.loadJobsWrapper - } - }); diff --git a/x-pack/legacy/plugins/ml/public/application/util/chart_utils.js b/x-pack/legacy/plugins/ml/public/application/util/chart_utils.js index 8aa933eb5e53f..110795c2d0290 100644 --- a/x-pack/legacy/plugins/ml/public/application/util/chart_utils.js +++ b/x-pack/legacy/plugins/ml/public/application/util/chart_utils.js @@ -12,7 +12,6 @@ import { MULTI_BUCKET_IMPACT } from '../../../common/constants/multi_bucket_impa import moment from 'moment'; import rison from 'rison-node'; -import chrome from 'ui/chrome'; import { timefilter } from 'ui/timefilter'; import { CHART_TYPE } from '../explorer/explorer_constants'; @@ -229,7 +228,7 @@ export function getExploreSeriesLink(series) { } }); - return `${chrome.getBasePath()}/app/ml#/timeseriesexplorer?_g=${_g}&_a=${encodeURIComponent(_a)}`; + return `#/timeseriesexplorer?_g=${_g}&_a=${encodeURIComponent(_a)}`; } export function showMultiBucketAnomalyMarker(point) { diff --git a/x-pack/legacy/plugins/ml/public/application/util/chart_utils.test.js b/x-pack/legacy/plugins/ml/public/application/util/chart_utils.test.js index a229113826a2e..5ef6b592ae271 100644 --- a/x-pack/legacy/plugins/ml/public/application/util/chart_utils.test.js +++ b/x-pack/legacy/plugins/ml/public/application/util/chart_utils.test.js @@ -58,7 +58,7 @@ timefilter.setTime({ describe('getExploreSeriesLink', () => { test('get timeseriesexplorer link', () => { const link = getExploreSeriesLink(seriesConfig); - const expectedLink = `/app/ml#/timeseriesexplorer?_g=(ml:(jobIds:!(population-03)),` + + const expectedLink = `#/timeseriesexplorer?_g=(ml:(jobIds:!(population-03)),` + `refreshInterval:(display:Off,pause:!f,value:0),time:(from:'2017-02-23T00:00:00.000Z',mode:absolute,` + `to:'2017-02-23T23:59:59.999Z'))&_a=(mlTimeSeriesExplorer%3A(detectorIndex%3A0%2Centities%3A` + `(nginx.access.remote_ip%3A'72.57.0.53')%2Czoom%3A(from%3A'2017-02-19T20%3A00%3A00.000Z'%2Cto%3A'2017-02-27T04%3A00%3A00.000Z'))` + diff --git a/x-pack/legacy/plugins/ml/public/application/util/index_utils.ts b/x-pack/legacy/plugins/ml/public/application/util/index_utils.ts index 99882b0243be8..2b8838c04cf69 100644 --- a/x-pack/legacy/plugins/ml/public/application/util/index_utils.ts +++ b/x-pack/legacy/plugins/ml/public/application/util/index_utils.ts @@ -6,19 +6,17 @@ import { toastNotifications } from 'ui/notify'; import { i18n } from '@kbn/i18n'; -import { SavedObjectAttributes, SimpleSavedObject } from 'kibana/public'; import chrome from 'ui/chrome'; -import { npStart } from 'ui/new_platform'; -import { SavedSearchLoader } from '../../../../../../../src/legacy/core_plugins/kibana/public/discover/types'; +import { Query } from 'src/plugins/data/public'; import { IndexPattern, IndexPatternsContract } from '../../../../../../../src/plugins/data/public'; - -type IndexPatternSavedObject = SimpleSavedObject; +import { IndexPatternSavedObject, SavedSearchSavedObject } from '../../../common/types/kibana'; let indexPatternCache: IndexPatternSavedObject[] = []; -let fullIndexPatterns: IndexPatternsContract | null = null; +let savedSearchesCache: SavedSearchSavedObject[] = []; +let indexPatternsContract: IndexPatternsContract | null = null; -export function loadIndexPatterns() { - fullIndexPatterns = npStart.plugins.data.indexPatterns; +export function loadIndexPatterns(indexPatterns: IndexPatternsContract) { + indexPatternsContract = indexPatterns; const savedObjectsClient = chrome.getSavedObjectsClient(); return savedObjectsClient .find({ @@ -32,10 +30,33 @@ export function loadIndexPatterns() { }); } +export function loadSavedSearches() { + const savedObjectsClient = chrome.getSavedObjectsClient(); + return savedObjectsClient + .find({ + type: 'search', + perPage: 10000, + }) + .then(response => { + savedSearchesCache = response.savedObjects; + return savedSearchesCache; + }); +} + +export async function loadSavedSearchById(id: string) { + const savedObjectsClient = chrome.getSavedObjectsClient(); + const ss = await savedObjectsClient.get('search', id); + return ss.error === undefined ? ss : null; +} + export function getIndexPatterns() { return indexPatternCache; } +export function getIndexPatternsContract() { + return indexPatternsContract; +} + export function getIndexPatternNames() { return indexPatternCache.map(i => i.attributes && i.attributes.title); } @@ -49,27 +70,44 @@ export function getIndexPatternIdFromName(name: string) { return null; } -export function loadCurrentIndexPattern( - indexPatterns: IndexPatternsContract, - $route: Record -) { - fullIndexPatterns = indexPatterns; - return fullIndexPatterns.get($route.current.params.index); +export async function getIndexPatternAndSavedSearch(savedSearchId: string) { + const resp: { savedSearch: SavedSearchSavedObject | null; indexPattern: IndexPattern | null } = { + savedSearch: null, + indexPattern: null, + }; + + if (savedSearchId === undefined) { + return resp; + } + + const ss = await loadSavedSearchById(savedSearchId); + if (ss === null) { + return resp; + } + const indexPatternId = ss.references.find(r => r.type === 'index-pattern')?.id; + resp.indexPattern = await getIndexPatternById(indexPatternId!); + resp.savedSearch = ss; + return resp; +} + +export function getQueryFromSavedSearch(savedSearch: SavedSearchSavedObject) { + const search = savedSearch.attributes.kibanaSavedObjectMeta as { searchSourceJSON: string }; + return JSON.parse(search.searchSourceJSON) as { + query: Query; + filter: any[]; + }; } export function getIndexPatternById(id: string): Promise { - if (fullIndexPatterns !== null) { - return fullIndexPatterns.get(id); + if (indexPatternsContract !== null) { + return indexPatternsContract.get(id); } else { throw new Error('Index patterns are not initialized!'); } } -export function loadCurrentSavedSearch( - savedSearches: SavedSearchLoader, - $route: Record -) { - return savedSearches.get($route.current.params.savedSearchId); +export function getSavedSearchById(id: string): SavedSearchSavedObject | undefined { + return savedSearchesCache.find(s => s.id === id); } /** diff --git a/x-pack/legacy/plugins/ml/public/index.scss b/x-pack/legacy/plugins/ml/public/index.scss deleted file mode 100644 index ac3f3fef97c70..0000000000000 --- a/x-pack/legacy/plugins/ml/public/index.scss +++ /dev/null @@ -1,45 +0,0 @@ -// Should import both the EUI constants and any Kibana ones that are considered global -@import 'src/legacy/ui/public/styles/styling_constants'; - -// ML has it's own variables for coloring -@import 'application/variables'; - -// Kibana management page ML section -#kibanaManagementMLSection { - @import 'application/management/index'; -} - -// Protect the rest of Kibana from ML generic namespacing -// SASSTODO: Prefix ml selectors instead -#ml-app { - // App level - @import 'application/app'; - - // Sub applications - @import 'application/data_frame_analytics/index'; - @import 'application/datavisualizer/index'; - @import 'application/explorer/index'; // SASSTODO: This file needs to be rewritten - @import 'application/jobs/index'; // SASSTODO: This collection of sass files has multiple problems - @import 'application/overview/index'; - @import 'application/settings/index'; - @import 'application/timeseriesexplorer/index'; - - // Components - @import 'application/components/annotations/annotation_description_list/index'; // SASSTODO: This file overwrites EUI directly - @import 'application/components/anomalies_table/index'; // SASSTODO: This file overwrites EUI directly - @import 'application/components/chart_tooltip/index'; - @import 'application/components/controls/index'; - @import 'application/components/entity_cell/index'; - @import 'application/components/field_title_bar/index'; - @import 'application/components/field_type_icon/index'; - @import 'application/components/influencers_list/index'; - @import 'application/components/items_grid/index'; - @import 'application/components/job_selector/index'; - @import 'application/components/loading_indicator/index'; // SASSTODO: This component should be replaced with EuiLoadingSpinner - @import 'application/components/navigation_menu/index'; - @import 'application/components/rule_editor/index'; // SASSTODO: This file overwrites EUI directly - @import 'application/components/stats_bar/index'; - - // Hacks are last so they can overwrite anything above if needed - @import 'application/hacks'; -} diff --git a/x-pack/legacy/plugins/ml/public/index.ts b/x-pack/legacy/plugins/ml/public/index.ts new file mode 100755 index 0000000000000..0057983104cc0 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { PluginInitializer } from '../../../../../src/core/public'; +import { MlPlugin, MlPluginSetup, MlPluginStart } from './plugin'; + +export const plugin: PluginInitializer = () => new MlPlugin(); + +export { MlPluginSetup, MlPluginStart }; diff --git a/x-pack/legacy/plugins/ml/public/legacy.ts b/x-pack/legacy/plugins/ml/public/legacy.ts new file mode 100644 index 0000000000000..3e007a18f4c5a --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/legacy.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { npSetup, npStart } from 'ui/new_platform'; + +import { PluginInitializerContext } from '../../../../../src/core/public'; +import { plugin } from '.'; + +const pluginInstance = plugin({} as PluginInitializerContext); + +export const setup = pluginInstance.setup(npSetup.core, { + npData: npStart.plugins.data, +}); +export const start = pluginInstance.start(npStart.core, npStart.plugins); diff --git a/x-pack/legacy/plugins/ml/public/plugin.ts b/x-pack/legacy/plugins/ml/public/plugin.ts new file mode 100644 index 0000000000000..2a80b18446046 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/plugin.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Plugin as DataPlugin } from 'src/plugins/data/public'; +import { Plugin, CoreStart, CoreSetup } from '../../../../../src/core/public'; + +export interface MlSetupDependencies { + npData: ReturnType; +} + +export interface MlStartDependencies { + __LEGACY: { + Storage: any; + xpackInfo: any; + }; +} + +export class MlPlugin implements Plugin { + setup(core: CoreSetup, { npData }: MlSetupDependencies) { + core.application.register({ + id: 'ml', + title: 'Machine learning', + async mount(context, params) { + const { renderApp } = await import('./application/app'); + return renderApp(context, { + ...params, + indexPatterns: npData.indexPatterns, + npData, + }); + }, + }); + + return {}; + } + + start(core: CoreStart, deps: {}) { + return {}; + } + public stop() {} +} + +export type MlPluginSetup = ReturnType; +export type MlPluginStart = ReturnType; diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job/line_chart.ts b/x-pack/legacy/plugins/ml/server/models/job_service/new_job/line_chart.ts index eb2f50b8e9250..5bb0f39982146 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_service/new_job/line_chart.ts +++ b/x-pack/legacy/plugins/ml/server/models/job_service/new_job/line_chart.ts @@ -128,6 +128,14 @@ function getSearchJsonFromConfig( }, }; + if (query.bool === undefined) { + query.bool = { + must: [], + }; + } else if (query.bool.must === undefined) { + query.bool.must = []; + } + query.bool.must.push({ range: { [timeField]: { diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 054382ed0fa81..2e7d4233dd7ff 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -7118,7 +7118,6 @@ "xpack.ml.itemsGrid.noItemsAddedTitle": "項目が追加されていません", "xpack.ml.itemsGrid.noMatchingItemsTitle": "一致する項目が見つかりません。", "xpack.ml.jobsBreadcrumbs.advancedConfigurationLabel": "高度な構成", - "xpack.ml.jobsBreadcrumbs.createJobLabel": "ジョブを作成", "xpack.ml.jobsBreadcrumbs.multiMetricLabel": "マルチメトリック", "xpack.ml.jobsBreadcrumbs.populationLabel": "集団", "xpack.ml.jobsBreadcrumbs.selectIndexOrSearchLabel": "インデックスまたは検索を選択", @@ -7693,7 +7692,6 @@ "xpack.ml.validateJob.modal.linkToJobTipsText.mlJobTipsLinkText": "機械学習ジョブのヒント", "xpack.ml.validateJob.modal.validateJobTitle": "ジョブ {title} の検証", "xpack.ml.validateJob.validateJobButtonLabel": "ジョブを検証", - "xpack.ml.dataFrameAnalyticsBreadcrumbs.dataFrameLabel": "分析", "xpack.ml.datavisualizer.actionsPanel.advancedDescription": "より高度なユースケースでは、ジョブの作成にすべてのオプションを使用します", "xpack.ml.datavisualizer.actionsPanel.advancedTitle": "高度な設定", "xpack.ml.datavisualizer.actionsPanel.createJobDescription": "高度なジョブウィザードでジョブを作成し、このデータの異常を検出します:", @@ -8045,7 +8043,6 @@ "xpack.ml.overview.statsBar.runningAnalyticsLabel": "実行中", "xpack.ml.overview.statsBar.stoppedAnalyticsLabel": "停止中", "xpack.ml.overview.statsBar.totalAnalyticsLabel": "分析ジョブ合計", - "xpack.ml.overviewBreadcrumbs.overviewLabel": "概要", "xpack.ml.overviewJobsList.statsBar.activeMLNodesLabel": "アクティブな ML ノード", "xpack.ml.overviewJobsList.statsBar.closedJobsLabel": "ジョブを作成", "xpack.ml.overviewJobsList.statsBar.failedJobsLabel": "失敗したジョブ", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 1977da8ac9100..fd5573901fb00 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -7120,7 +7120,6 @@ "xpack.ml.itemsGrid.noItemsAddedTitle": "没有添加任何项", "xpack.ml.itemsGrid.noMatchingItemsTitle": "没有匹配的项", "xpack.ml.jobsBreadcrumbs.advancedConfigurationLabel": "高级配置", - "xpack.ml.jobsBreadcrumbs.createJobLabel": "创建作业", "xpack.ml.jobsBreadcrumbs.multiMetricLabel": "多指标", "xpack.ml.jobsBreadcrumbs.populationLabel": "填充", "xpack.ml.jobsBreadcrumbs.selectIndexOrSearchLabel": "选择索引或搜索", @@ -7786,7 +7785,6 @@ "xpack.ml.dataframe.analyticsList.type": "类型", "xpack.ml.dataframe.analyticsList.viewActionName": "查看", "xpack.ml.dataframe.analyticsList.viewAriaLabel": "查看", - "xpack.ml.dataFrameAnalyticsBreadcrumbs.dataFrameLabel": "分析", "xpack.ml.datavisualizer.actionsPanel.advancedDescription": "使用全部选项为更高级的用例创建作业", "xpack.ml.datavisualizer.actionsPanel.advancedTitle": "高级", "xpack.ml.datavisualizer.actionsPanel.createJobDescription": "使用“高级作业”向导创建作业,以查找此数据中的异常:", @@ -8138,7 +8136,6 @@ "xpack.ml.overview.statsBar.runningAnalyticsLabel": "正在运行", "xpack.ml.overview.statsBar.stoppedAnalyticsLabel": "已停止", "xpack.ml.overview.statsBar.totalAnalyticsLabel": "分析作业总数", - "xpack.ml.overviewBreadcrumbs.overviewLabel": "概览", "xpack.ml.overviewJobsList.statsBar.activeMLNodesLabel": "活动 ML 节点", "xpack.ml.overviewJobsList.statsBar.closedJobsLabel": "已关闭的作业", "xpack.ml.overviewJobsList.statsBar.failedJobsLabel": "失败的作业", From a12d8551a1c89fc674653c315e2947ff8d4492e5 Mon Sep 17 00:00:00 2001 From: "Devin W. Hurley" Date: Wed, 11 Dec 2019 11:09:36 -0500 Subject: [PATCH 30/40] [SIEM] [Detection Engine] Search signals index (#52661) * adds route for querying signals index, also updates signal status type names * first pass at happy path tests * fixes stuff after rebase with master * utilizes removes search_query from payload and replaces it with just query, adds aggs to signals search api, updates route and validation tests * removes _headers parameter from route handler and updates comment for aggs script --- .../legacy/plugins/siem/common/constants.ts | 1 + .../plugins/siem/server/kibana.index.ts | 2 + .../routes/__mocks__/request_responses.ts | 29 +++- .../query_signals_index_schema.test.ts | 39 ++++++ .../schemas/query_signals_index_schema.ts | 12 ++ .../schemas/set_signal_status_schema.test.ts | 14 +- .../signals/open_close_signals_route.ts | 4 +- .../signals/query_signals_route.test.ts | 129 ++++++++++++++++++ .../routes/signals/query_signals_route.ts | 47 +++++++ .../scripts/signals/aggs_signals.sh | 19 +++ .../scripts/signals/query_signals.sh | 19 +++ .../lib/detection_engine/signals/types.ts | 25 +++- 12 files changed, 323 insertions(+), 17 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_signals_index_schema.test.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_signals_index_schema.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.test.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.ts create mode 100755 x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/aggs_signals.sh create mode 100755 x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/query_signals.sh diff --git a/x-pack/legacy/plugins/siem/common/constants.ts b/x-pack/legacy/plugins/siem/common/constants.ts index 0924b6c6eb5e6..c3494c0969900 100644 --- a/x-pack/legacy/plugins/siem/common/constants.ts +++ b/x-pack/legacy/plugins/siem/common/constants.ts @@ -53,3 +53,4 @@ export const DETECTION_ENGINE_INDEX_URL = `${DETECTION_ENGINE_URL}/index`; export const SIGNALS_INDEX_KEY = 'signalsIndex'; export const DETECTION_ENGINE_SIGNALS_URL = `${DETECTION_ENGINE_URL}/signals`; export const DETECTION_ENGINE_SIGNALS_STATUS_URL = `${DETECTION_ENGINE_SIGNALS_URL}/status`; +export const DETECTION_ENGINE_QUERY_SIGNALS_URL = `${DETECTION_ENGINE_SIGNALS_URL}/search`; diff --git a/x-pack/legacy/plugins/siem/server/kibana.index.ts b/x-pack/legacy/plugins/siem/server/kibana.index.ts index f56e6b3c3f550..65b673e1c72a5 100644 --- a/x-pack/legacy/plugins/siem/server/kibana.index.ts +++ b/x-pack/legacy/plugins/siem/server/kibana.index.ts @@ -15,6 +15,7 @@ import { findRulesRoute } from './lib/detection_engine/routes/rules/find_rules_r import { deleteRulesRoute } from './lib/detection_engine/routes/rules/delete_rules_route'; import { updateRulesRoute } from './lib/detection_engine/routes/rules/update_rules_route'; import { setSignalsStatusRoute } from './lib/detection_engine/routes/signals/open_close_signals_route'; +import { querySignalsRoute } from './lib/detection_engine/routes/signals/query_signals_route'; import { ServerFacade } from './types'; import { deleteIndexRoute } from './lib/detection_engine/routes/index/delete_index_route'; import { isAlertExecutor } from './lib/detection_engine/signals/types'; @@ -44,6 +45,7 @@ export const initServerWithKibana = (context: PluginInitializerContext, __legacy // POST /api/detection_engine/signals/status // Example usage can be found in siem/server/lib/detection_engine/scripts/signals setSignalsStatusRoute(__legacy); + querySignalsRoute(__legacy); // Detection Engine index routes that have the REST endpoints of /api/detection_engine/index // All REST index creation, policy management for spaces diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts index 978434859ef95..86726187c4fbd 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts @@ -6,10 +6,11 @@ import { ServerInjectOptions } from 'hapi'; import { ActionResult } from '../../../../../../actions/server/types'; -import { SignalsRestParams } from '../../signals/types'; +import { SignalsStatusRestParams, SignalsQueryRestParams } from '../../signals/types'; import { DETECTION_ENGINE_RULES_URL, DETECTION_ENGINE_SIGNALS_STATUS_URL, + DETECTION_ENGINE_QUERY_SIGNALS_URL, } from '../../../../../common/constants'; import { RuleAlertType } from '../../rules/types'; import { RuleAlertParamsRest } from '../../types'; @@ -40,17 +41,25 @@ export const typicalPayload = (): Partial> = ], }); -export const typicalSetStatusSignalByIdsPayload = (): Partial => ({ +export const typicalSetStatusSignalByIdsPayload = (): Partial => ({ signal_ids: ['somefakeid1', 'somefakeid2'], status: 'closed', }); -export const typicalSetStatusSignalByQueryPayload = (): Partial => ({ +export const typicalSetStatusSignalByQueryPayload = (): Partial => ({ query: { range: { '@timestamp': { gte: 'now-2M', lte: 'now/M' } } }, status: 'closed', }); -export const setStatusSignalMissingIdsAndQueryPayload = (): Partial => ({ +export const typicalSignalsQuery = (): Partial => ({ + query: { match_all: {} }, +}); + +export const typicalSignalsQueryAggs = (): Partial => ({ + aggs: { statuses: { terms: { field: 'signal.status', size: 10 } } }, +}); + +export const setStatusSignalMissingIdsAndQueryPayload = (): Partial => ({ status: 'closed', }); @@ -134,6 +143,18 @@ export const getSetSignalStatusByQueryRequest = (): ServerInjectOptions => ({ }, }); +export const getSignalsQueryRequest = (): ServerInjectOptions => ({ + method: 'POST', + url: DETECTION_ENGINE_QUERY_SIGNALS_URL, + payload: { ...typicalSignalsQuery() }, +}); + +export const getSignalsAggsQueryRequest = (): ServerInjectOptions => ({ + method: 'POST', + url: DETECTION_ENGINE_QUERY_SIGNALS_URL, + payload: { ...typicalSignalsQueryAggs() }, +}); + export const createActionResult = (): ActionResult => ({ id: 'result-1', actionTypeId: 'action-id-1', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_signals_index_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_signals_index_schema.test.ts new file mode 100644 index 0000000000000..4f0dbf10f4559 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_signals_index_schema.test.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { querySignalsSchema } from './query_signals_index_schema'; +import { SignalsQueryRestParams } from '../../signals/types'; + +describe('query and aggs on signals index', () => { + test('query and aggs simultaneously', () => { + expect( + querySignalsSchema.validate>({ + query: {}, + aggs: {}, + }).error + ).toBeFalsy(); + }); + + test('query only', () => { + expect( + querySignalsSchema.validate>({ + query: {}, + }).error + ).toBeFalsy(); + }); + + test('aggs only', () => { + expect( + querySignalsSchema.validate>({ + aggs: {}, + }).error + ).toBeFalsy(); + }); + + test('missing query and aggs is invalid', () => { + expect(querySignalsSchema.validate>({}).error).toBeTruthy(); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_signals_index_schema.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_signals_index_schema.ts new file mode 100644 index 0000000000000..53ce50692e84a --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_signals_index_schema.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Joi from 'joi'; + +export const querySignalsSchema = Joi.object({ + query: Joi.object(), + aggs: Joi.object(), +}).min(1); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/set_signal_status_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/set_signal_status_schema.test.ts index b586b4666bfee..792c7afad05b1 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/set_signal_status_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/set_signal_status_schema.test.ts @@ -5,12 +5,12 @@ */ import { setSignalsStatusSchema } from './set_signal_status_schema'; -import { SignalsRestParams } from '../../signals/types'; +import { SignalsStatusRestParams } from '../../signals/types'; describe('set signal status schema', () => { test('signal_ids and status is valid', () => { expect( - setSignalsStatusSchema.validate>({ + setSignalsStatusSchema.validate>({ signal_ids: ['somefakeid'], status: 'open', }).error @@ -19,7 +19,7 @@ describe('set signal status schema', () => { test('query and status is valid', () => { expect( - setSignalsStatusSchema.validate>({ + setSignalsStatusSchema.validate>({ query: {}, status: 'open', }).error @@ -28,7 +28,7 @@ describe('set signal status schema', () => { test('signal_ids and missing status is invalid', () => { expect( - setSignalsStatusSchema.validate>({ + setSignalsStatusSchema.validate>({ signal_ids: ['somefakeid'], }).error ).toBeTruthy(); @@ -36,7 +36,7 @@ describe('set signal status schema', () => { test('query and missing status is invalid', () => { expect( - setSignalsStatusSchema.validate>({ + setSignalsStatusSchema.validate>({ query: {}, }).error ).toBeTruthy(); @@ -44,7 +44,7 @@ describe('set signal status schema', () => { test('status is present but query or signal_ids is missing is invalid', () => { expect( - setSignalsStatusSchema.validate>({ + setSignalsStatusSchema.validate>({ status: 'closed', }).error ).toBeTruthy(); @@ -54,7 +54,7 @@ describe('set signal status schema', () => { expect( setSignalsStatusSchema.validate< Partial< - Omit & { + Omit & { status: string; } > diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals_route.ts index b342cc5cd14ef..7c49a1942ee91 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals_route.ts @@ -6,7 +6,7 @@ import Hapi from 'hapi'; import { DETECTION_ENGINE_SIGNALS_STATUS_URL } from '../../../../../common/constants'; -import { SignalsRequest } from '../../signals/types'; +import { SignalsStatusRequest } from '../../signals/types'; import { setSignalsStatusSchema } from '../schemas/set_signal_status_schema'; import { ServerFacade } from '../../../../types'; import { transformError, getIndex } from '../utils'; @@ -24,7 +24,7 @@ export const setSignalsStatusRouteDef = (server: ServerFacade): Hapi.ServerRoute payload: setSignalsStatusSchema, }, }, - async handler(request: SignalsRequest, headers) { + async handler(request: SignalsStatusRequest) { const { signal_ids: signalIds, query, status } = request.payload; const index = getIndex(request, server); const { callWithRequest } = server.plugins.elasticsearch.getCluster('data'); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.test.ts new file mode 100644 index 0000000000000..1b990e8c1ff57 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.test.ts @@ -0,0 +1,129 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createMockServer } from '../__mocks__/_mock_server'; +import { querySignalsRoute } from './query_signals_route'; +import * as myUtils from '../utils'; +import { ServerInjectOptions } from 'hapi'; +import { + getSignalsQueryRequest, + getSignalsAggsQueryRequest, + typicalSignalsQuery, + typicalSignalsQueryAggs, +} from '../__mocks__/request_responses'; +import { DETECTION_ENGINE_QUERY_SIGNALS_URL } from '../../../../../common/constants'; + +describe('query for signal', () => { + let { server, elasticsearch } = createMockServer(); + + beforeEach(() => { + jest.resetAllMocks(); + jest.spyOn(myUtils, 'getIndex').mockReturnValue('fakeindex'); + ({ server, elasticsearch } = createMockServer()); + elasticsearch.getCluster = jest.fn(() => ({ + callWithRequest: jest.fn(() => true), + })); + querySignalsRoute(server); + }); + + describe('query and agg on signals index', () => { + test('returns 200 when using single query', async () => { + elasticsearch.getCluster = jest.fn(() => ({ + callWithRequest: jest.fn( + (_req, _type: string, queryBody: { index: string; body: object }) => { + expect(queryBody.body).toMatchObject({ ...typicalSignalsQueryAggs() }); + return true; + } + ), + })); + const { statusCode } = await server.inject(getSignalsAggsQueryRequest()); + expect(statusCode).toBe(200); + expect(myUtils.getIndex).toHaveReturnedWith('fakeindex'); + }); + + test('returns 200 when using single agg', async () => { + elasticsearch.getCluster = jest.fn(() => ({ + callWithRequest: jest.fn( + (_req, _type: string, queryBody: { index: string; body: object }) => { + expect(queryBody.body).toMatchObject({ ...typicalSignalsQueryAggs() }); + return true; + } + ), + })); + const { statusCode } = await server.inject(getSignalsAggsQueryRequest()); + expect(statusCode).toBe(200); + expect(myUtils.getIndex).toHaveReturnedWith('fakeindex'); + }); + + test('returns 200 when using aggs and query together', async () => { + const allTogether = getSignalsQueryRequest(); + allTogether.payload = { ...typicalSignalsQueryAggs(), ...typicalSignalsQuery() }; + elasticsearch.getCluster = jest.fn(() => ({ + callWithRequest: jest.fn( + (_req, _type: string, queryBody: { index: string; body: object }) => { + expect(queryBody.body).toMatchObject({ + ...typicalSignalsQueryAggs(), + ...typicalSignalsQuery(), + }); + return true; + } + ), + })); + const { statusCode } = await server.inject(allTogether); + expect(statusCode).toBe(200); + expect(myUtils.getIndex).toHaveReturnedWith('fakeindex'); + }); + + test('returns 400 when missing aggs and query', async () => { + const allTogether = getSignalsQueryRequest(); + allTogether.payload = {}; + const { statusCode } = await server.inject(allTogether); + expect(statusCode).toBe(400); + }); + }); + + describe('validation', () => { + test('returns 200 if query present', async () => { + const request: ServerInjectOptions = { + method: 'POST', + url: DETECTION_ENGINE_QUERY_SIGNALS_URL, + payload: typicalSignalsQuery(), + }; + const { statusCode } = await server.inject(request); + expect(statusCode).toBe(200); + }); + + test('returns 200 if aggs is present', async () => { + const request: ServerInjectOptions = { + method: 'POST', + url: DETECTION_ENGINE_QUERY_SIGNALS_URL, + payload: typicalSignalsQueryAggs(), + }; + const { statusCode } = await server.inject(request); + expect(statusCode).toBe(200); + }); + + test('returns 200 if aggs and query are present', async () => { + const request: ServerInjectOptions = { + method: 'POST', + url: DETECTION_ENGINE_QUERY_SIGNALS_URL, + payload: { ...typicalSignalsQueryAggs(), ...typicalSignalsQuery() }, + }; + const { statusCode } = await server.inject(request); + expect(statusCode).toBe(200); + }); + + test('returns 400 if aggs and query are NOT present', async () => { + const request: ServerInjectOptions = { + method: 'POST', + url: DETECTION_ENGINE_QUERY_SIGNALS_URL, + payload: {}, + }; + const { statusCode } = await server.inject(request); + expect(statusCode).toBe(400); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.ts new file mode 100644 index 0000000000000..89ffed259cf77 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Hapi from 'hapi'; +import { DETECTION_ENGINE_QUERY_SIGNALS_URL } from '../../../../../common/constants'; +import { SignalsQueryRequest } from '../../signals/types'; +import { querySignalsSchema } from '../schemas/query_signals_index_schema'; +import { ServerFacade } from '../../../../types'; +import { transformError, getIndex } from '../utils'; + +export const querySignalsRouteDef = (server: ServerFacade): Hapi.ServerRoute => { + return { + method: 'POST', + path: DETECTION_ENGINE_QUERY_SIGNALS_URL, + options: { + tags: ['access:siem'], + validate: { + options: { + abortEarly: false, + }, + payload: querySignalsSchema, + }, + }, + async handler(request: SignalsQueryRequest) { + const { query, aggs } = request.payload; + const body = { query, aggs }; + const index = getIndex(request, server); + const { callWithRequest } = server.plugins.elasticsearch.getCluster('data'); + try { + return callWithRequest(request, 'search', { + index, + body, + }); + } catch (exc) { + // error while getting or updating signal with id: id in signal index .siem-signals + return transformError(exc); + } + }, + }; +}; + +export const querySignalsRoute = (server: ServerFacade) => { + server.route(querySignalsRouteDef(server)); +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/aggs_signals.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/aggs_signals.sh new file mode 100755 index 0000000000000..27186a14af902 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/aggs_signals.sh @@ -0,0 +1,19 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +set -e +./check_env_variables.sh + +# Example: ./signals/aggs_signal.sh + curl -s -k \ + -H 'Content-Type: application/json' \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X POST ${KIBANA_URL}${SPACE_URL}/api/detection_engine/signals/search \ + -d '{"aggs": {"statuses": {"terms": {"field": "signal.status", "size": 10 }}}}' \ + | jq . diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/query_signals.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/query_signals.sh new file mode 100755 index 0000000000000..2fc76406ec0f6 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/query_signals.sh @@ -0,0 +1,19 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +set -e +./check_env_variables.sh + +# Example: ./signals/query_signals.sh + curl -s -k \ + -H 'Content-Type: application/json' \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X POST ${KIBANA_URL}${SPACE_URL}/api/detection_engine/signals/search \ + -d '{ "query": { "match_all": {} } } ' \ + | jq . diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/types.ts index 213ceb29a6e25..a30182c537884 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/types.ts @@ -15,12 +15,29 @@ export interface SignalsParams { status: 'open' | 'closed'; } -export type SignalsRestParams = Omit & { - signal_ids: SignalsParams['signalIds']; +export interface SignalsStatusParams { + signalIds: string[] | undefined | null; + query: object | undefined | null; + status: 'open' | 'closed'; +} + +export interface SignalQueryParams { + query: object | undefined | null; + aggs: object | undefined | null; +} + +export type SignalsStatusRestParams = Omit & { + signal_ids: SignalsStatusParams['signalIds']; }; -export interface SignalsRequest extends RequestFacade { - payload: SignalsRestParams; +export type SignalsQueryRestParams = SignalQueryParams; + +export interface SignalsStatusRequest extends RequestFacade { + payload: SignalsStatusRestParams; +} + +export interface SignalsQueryRequest extends RequestFacade { + payload: SignalsQueryRestParams; } export type SearchTypes = From 73938f0cf463b640661c88fe1fed2d777d3341b5 Mon Sep 17 00:00:00 2001 From: Mariana Dima Date: Wed, 11 Dec 2019 17:34:46 +0100 Subject: [PATCH 31/40] add azure data (#52669) --- .../azure_metrics/screenshot.png | Bin 0 -> 1259487 bytes .../home/tutorial_resources/logos/azure.svg | 17 +++++ .../server/tutorials/azure_metrics/index.js | 61 ++++++++++++++++++ .../kibana/server/tutorials/register.js | 2 + 4 files changed, 80 insertions(+) create mode 100644 src/legacy/core_plugins/kibana/public/home/tutorial_resources/azure_metrics/screenshot.png create mode 100644 src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/azure.svg create mode 100644 src/legacy/core_plugins/kibana/server/tutorials/azure_metrics/index.js diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/azure_metrics/screenshot.png b/src/legacy/core_plugins/kibana/public/home/tutorial_resources/azure_metrics/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..22136049b494ad9627683b0b3fff29cd83f0361a GIT binary patch literal 1259487 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≺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ƻgG#8PFd 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 z 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

{nHiKwL|Q~NpZV==ZS7)d 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 z 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 z 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

01>= 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

<@Mg5Pw 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 Ub?1Ls1U?Qpx$Y(HK6T}P0bI7e%m4rY literal 0 HcmV?d00001 diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/azure.svg b/src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/azure.svg new file mode 100644 index 0000000000000..f8df12ba05c50 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/azure.svg @@ -0,0 +1,17 @@ + + + + + + + + + diff --git a/src/legacy/core_plugins/kibana/server/tutorials/azure_metrics/index.js b/src/legacy/core_plugins/kibana/server/tutorials/azure_metrics/index.js new file mode 100644 index 0000000000000..8bd0d17b143cf --- /dev/null +++ b/src/legacy/core_plugins/kibana/server/tutorials/azure_metrics/index.js @@ -0,0 +1,61 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { TUTORIAL_CATEGORY } from '../../../common/tutorials/tutorial_category'; +import { onPremInstructions, cloudInstructions, onPremCloudInstructions } from '../../../common/tutorials/metricbeat_instructions'; + +export function azureMetricsSpecProvider(context) { + const moduleName = 'azure'; + return { + id: 'azureMetrics', + name: i18n.translate('kbn.server.tutorials.azureMetrics.nameTitle', { + defaultMessage: 'Azure metrics', + }), + isBeta: true, + category: TUTORIAL_CATEGORY.METRICS, + shortDescription: i18n.translate('kbn.server.tutorials.azureMetrics.shortDescription', { + defaultMessage: 'Fetch Azure Monitor metrics.', + }), + longDescription: i18n.translate('kbn.server.tutorials.azureMetrics.longDescription', { + defaultMessage: 'The `azure` Metricbeat module fetches Azure Monitor metrics. \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-azure.html', + }, + }), + euiIconType: 'logoAzure', + artifacts: { + application: { + label: i18n.translate('kbn.server.tutorials.azureMetrics.artifacts.application.label', { + defaultMessage: 'Discover', + }), + path: '/app/kibana#/discover' + }, + dashboards: [], + exportedFields: { + documentationUrl: '{config.docs.beats.metricbeat}/exported-fields-azure.html' + } + }, + completionTimeMinutes: 10, + onPrem: onPremInstructions(moduleName, null, null, null, context), + elasticCloud: cloudInstructions(moduleName), + onPremElasticCloud: onPremCloudInstructions(moduleName) + }; +} diff --git a/src/legacy/core_plugins/kibana/server/tutorials/register.js b/src/legacy/core_plugins/kibana/server/tutorials/register.js index 75f29a9f10e53..ec6ffe7e6a7ba 100644 --- a/src/legacy/core_plugins/kibana/server/tutorials/register.js +++ b/src/legacy/core_plugins/kibana/server/tutorials/register.js @@ -82,6 +82,7 @@ import { traefikMetricsSpecProvider } from './traefik_metrics'; import { awsLogsSpecProvider } from './aws_logs'; import { activemqLogsSpecProvider } from './activemq_logs'; import { activemqMetricsSpecProvider } from './activemq_metrics'; +import { azureMetricsSpecProvider } from './azure_metrics'; export function registerTutorials(server) { server.newPlatform.setup.plugins.home.tutorials.registerTutorial(systemLogsSpecProvider); @@ -150,4 +151,5 @@ export function registerTutorials(server) { server.newPlatform.setup.plugins.home.tutorials.registerTutorial(awsLogsSpecProvider); server.newPlatform.setup.plugins.home.tutorials.registerTutorial(activemqLogsSpecProvider); server.newPlatform.setup.plugins.home.tutorials.registerTutorial(activemqMetricsSpecProvider); + server.newPlatform.setup.plugins.home.tutorials.registerTutorial(azureMetricsSpecProvider); } From a25bf49eb8d6238013293ee4579e036139967d24 Mon Sep 17 00:00:00 2001 From: Spencer Date: Wed, 11 Dec 2019 09:42:43 -0700 Subject: [PATCH 32/40] Add failure screenshot links to JUnit failures (#52449) --- .../add_messages_to_report.test.ts | 49 ++++---- .../add_messages_to_report.ts | 14 +-- .../failed_tests_reporter/report_metadata.ts | 53 ++++++++ .../run_failed_tests_reporter_cli.ts | 46 +++---- .../src/failed_tests_reporter/test_report.ts | 26 ++-- .../functional_test_runner.ts | 3 + .../lib/failure_metadata.test.ts | 71 +++++++++++ .../lib/failure_metadata.ts | 104 +++++++++++++++ .../src/functional_test_runner/lib/index.ts | 1 + .../lib/lifecycle_event.ts | 68 ++++++++++ .../lib/mocha/reporter/reporter.js | 2 + .../__tests__/junit_report_generation.js | 4 + .../src/mocha/junit_report_generation.js | 2 + packages/kbn-test/types/ftr.d.ts | 7 +- test/functional/services/screenshots.ts | 10 +- vars/kibanaPipeline.groovy | 119 +++++++++--------- 16 files changed, 450 insertions(+), 129 deletions(-) create mode 100644 packages/kbn-test/src/failed_tests_reporter/report_metadata.ts create mode 100644 packages/kbn-test/src/functional_test_runner/lib/failure_metadata.test.ts create mode 100644 packages/kbn-test/src/functional_test_runner/lib/failure_metadata.ts create mode 100644 packages/kbn-test/src/functional_test_runner/lib/lifecycle_event.ts diff --git a/packages/kbn-test/src/failed_tests_reporter/add_messages_to_report.test.ts b/packages/kbn-test/src/failed_tests_reporter/add_messages_to_report.test.ts index 9e800e88bc9ba..5a7939f87130b 100644 --- a/packages/kbn-test/src/failed_tests_reporter/add_messages_to_report.test.ts +++ b/packages/kbn-test/src/failed_tests_reporter/add_messages_to_report.test.ts @@ -73,12 +73,17 @@ it('rewrites ftr reports with minimal changes', async () => { =================================================================== --- ftr.xml [object Object] +++ ftr.xml - @@ -2,52 +2,56 @@ + @@ -1,53 +1,56 @@ + ‹?xml version="1.0" encoding="utf-8"?› ‹testsuites› ‹testsuite timestamp="2019-06-05T23:37:10" time="903.670" tests="129" failures="5" skipped="71"› ‹testcase name="maps app maps loaded from sample data ecommerce "before all" hook" classname="Chrome X-Pack UI Functional Tests.x-pack/test/functional/apps/maps/sample_data·js" time="154.378"› - ‹system-out› + - ‹system-out› - ‹![CDATA[[00:00:00] │ + + ‹system-out›Failed Tests Reporter: + + - foo bar + + + + + [00:00:00] │ [00:07:04] └-: maps app ... @@ -94,13 +99,8 @@ it('rewrites ftr reports with minimal changes', async () => { at process._tickCallback (internal/process/next_tick.js:68:7) at lastError (/var/lib/jenkins/workspace/elastic+kibana+master/JOB/x-pack-ciGroup7/node/immutable/kibana/test/common/services/retry/retry_for_success.ts:28:9) - at onFailure (/var/lib/jenkins/workspace/elastic+kibana+master/JOB/x-pack-ciGroup7/node/immutable/kibana/test/common/services/retry/retry_for_success.ts:68:13)]]› - - ‹/failure› + at onFailure (/var/lib/jenkins/workspace/elastic+kibana+master/JOB/x-pack-ciGroup7/node/immutable/kibana/test/common/services/retry/retry_for_success.ts:68:13) - + - + - +Failed Tests Reporter: - + - foo bar - +‹/failure› + ‹/failure› ‹/testcase› ‹testcase name="maps app "after all" hook" classname="Chrome X-Pack UI Functional Tests.x-pack/test/functional/apps/maps" time="0.179"› ‹system-out› @@ -181,11 +181,11 @@ it('rewrites jest reports with minimal changes', async () => { + ‹failure›‹![CDATA[ + TypeError: Cannot read property '0' of undefined + at Object.‹anonymous›.test (/var/lib/jenkins/workspace/elastic+kibana+master/JOB/x-pack-intake/node/immutable/kibana/x-pack/legacy/plugins/code/server/lsp/abstract_launcher.test.ts:166:10) - + - + - +Failed Tests Reporter: + + ]]›‹/failure› + + ‹system-out›Failed Tests Reporter: + - foo bar - +]]›‹/failure› + + + +‹/system-out› ‹/testcase› ‹testcase classname="X-Pack Jest Tests.x-pack/legacy/plugins/code/server/lsp" name="passive launcher can start and end a process" time="0.435"/› ‹testcase classname="X-Pack Jest Tests.x-pack/legacy/plugins/code/server/lsp" name="passive launcher should restart a process if a process died before connected" time="1.502"/› @@ -216,12 +216,17 @@ it('rewrites mocha reports with minimal changes', async () => { =================================================================== --- mocha.xml [object Object] +++ mocha.xml - @@ -2,12 +2,12 @@ + @@ -1,13 +1,16 @@ + ‹?xml version="1.0" encoding="utf-8"?› ‹testsuites› ‹testsuite timestamp="2019-06-13T23:29:36" time="30.739" tests="1444" failures="2" skipped="3"› ‹testcase name="code in multiple nodes "before all" hook" classname="X-Pack Mocha Tests.x-pack/legacy/plugins/code/server/__tests__/multi_node·ts" time="0.121"› - ‹system-out› + - ‹system-out› - ‹![CDATA[]]› + + ‹system-out›Failed Tests Reporter: + + - foo bar + + + + + ‹/system-out› - ‹failure› @@ -232,7 +237,7 @@ it('rewrites mocha reports with minimal changes', async () => { ‹head›‹title›503 Service Temporarily Unavailable‹/title›‹/head› ‹body bgcolor="white"› ‹center›‹h1›503 Service Temporarily Unavailable‹/h1›‹/center› - @@ -15,24 +15,28 @@ + @@ -15,24 +18,24 @@ ‹/body› ‹/html› @@ -240,11 +245,7 @@ it('rewrites mocha reports with minimal changes', async () => { - at process._tickCallback (internal/process/next_tick.js:68:7)]]› - ‹/failure› + at process._tickCallback (internal/process/next_tick.js:68:7) - + - + - +Failed Tests Reporter: - + - foo bar - +]]›‹/failure› + + ]]›‹/failure› ‹/testcase› ‹testcase name="code in multiple nodes "after all" hook" classname="X-Pack Mocha Tests.x-pack/legacy/plugins/code/server/__tests__/multi_node·ts" time="0.003"› ‹system-out› @@ -324,11 +325,11 @@ it('rewrites karma reports with minimal changes', async () => { + at Generator.prototype.‹computed› [as next] (webpack://%5Bname%5D/./node_modules/regenerator-runtime/runtime.js?:114:21) + at asyncGeneratorStep (http://localhost:5610/bundles/tests.bundle.js?shards=4&shard_num=1:158772:103) + at _next (http://localhost:5610/bundles/tests.bundle.js?shards=4&shard_num=1:158774:194) - + - + - +Failed Tests Reporter: - + - foo bar +]]›‹/failure› + + ‹system-out›Failed Tests Reporter: + + - foo bar + + + +‹/system-out› ‹/testcase› ‹testcase name="CoordinateMapsVisualizationTest CoordinateMapsVisualization - basics should toggle to Heatmap OK" time="0.055" classname="Browser Unit Tests.CoordinateMapsVisualizationTest"/› ‹testcase name="VegaParser._parseSchema should warn on vega-lite version too new to be supported" time="0.001" classname="Browser Unit Tests.VegaParser·_parseSchema"/› diff --git a/packages/kbn-test/src/failed_tests_reporter/add_messages_to_report.ts b/packages/kbn-test/src/failed_tests_reporter/add_messages_to_report.ts index f82e1ef1fc19a..32ea5fa0f9033 100644 --- a/packages/kbn-test/src/failed_tests_reporter/add_messages_to_report.ts +++ b/packages/kbn-test/src/failed_tests_reporter/add_messages_to_report.ts @@ -57,16 +57,14 @@ export async function addMessagesToReport(options: { } log.info(`${classname} - ${name}:${messageList}`); - const append = `\n\nFailed Tests Reporter:${messageList}\n`; + const output = `Failed Tests Reporter:${messageList}\n\n`; - if ( - testCase.failure[0] && - typeof testCase.failure[0] === 'object' && - typeof testCase.failure[0]._ === 'string' - ) { - testCase.failure[0]._ += append; + if (!testCase['system-out']) { + testCase['system-out'] = [output]; + } else if (typeof testCase['system-out'][0] === 'string') { + testCase['system-out'][0] = output + String(testCase['system-out'][0]); } else { - testCase.failure[0] = String(testCase.failure[0]) + append; + testCase['system-out'][0]._ = output + testCase['system-out'][0]._; } } diff --git a/packages/kbn-test/src/failed_tests_reporter/report_metadata.ts b/packages/kbn-test/src/failed_tests_reporter/report_metadata.ts new file mode 100644 index 0000000000000..aad4c5d3c30c0 --- /dev/null +++ b/packages/kbn-test/src/failed_tests_reporter/report_metadata.ts @@ -0,0 +1,53 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { TestReport, makeTestCaseIter } from './test_report'; +import { Message } from './add_messages_to_report'; + +export function* getMetadataIter(report: TestReport) { + for (const testCase of makeTestCaseIter(report)) { + if (!testCase.$['metadata-json']) { + yield [{}, testCase]; + } else { + yield [{}, JSON.parse(testCase.$['metadata-json'])]; + } + } +} + +export function getReportMessages(report: TestReport) { + const messages: Message[] = []; + for (const [metadata, testCase] of getMetadataIter(report)) { + for (const message of metadata.messages || []) { + messages.push({ + classname: testCase.$.classname, + name: testCase.$.name, + message, + }); + } + + for (const screenshot of metadata.screenshots || []) { + messages.push({ + classname: testCase.$.classname, + name: testCase.$.name, + message: `Screenshot: ${screenshot.name} ${screenshot.url}`, + }); + } + } + return messages; +} diff --git a/packages/kbn-test/src/failed_tests_reporter/run_failed_tests_reporter_cli.ts b/packages/kbn-test/src/failed_tests_reporter/run_failed_tests_reporter_cli.ts index b3c2a8dc338da..fd7976d6e87e1 100644 --- a/packages/kbn-test/src/failed_tests_reporter/run_failed_tests_reporter_cli.ts +++ b/packages/kbn-test/src/failed_tests_reporter/run_failed_tests_reporter_cli.ts @@ -25,7 +25,8 @@ import { GithubApi } from './github_api'; import { updateFailureIssue, createFailureIssue } from './report_failure'; import { getIssueMetadata } from './issue_metadata'; import { readTestReport } from './test_report'; -import { addMessagesToReport, Message } from './add_messages_to_report'; +import { addMessagesToReport } from './add_messages_to_report'; +import { getReportMessages } from './report_metadata'; export function runFailedTestsReporterCli() { run( @@ -74,17 +75,22 @@ export function runFailedTestsReporterCli() { for (const reportPath of reportPaths) { const report = await readTestReport(reportPath); - const messages: Message[] = []; + const messages = getReportMessages(report); for (const failure of await getFailures(report)) { - if (failure.likelyIrrelevant) { + const pushMessage = (msg: string) => { messages.push({ classname: failure.classname, name: failure.name, - message: - 'Failure is likely irrelevant' + - (updateGithub ? ', so an issue was not created or updated' : ''), + message: msg, }); + }; + + if (failure.likelyIrrelevant) { + pushMessage( + 'Failure is likely irrelevant' + + (updateGithub ? ', so an issue was not created or updated' : '') + ); continue; } @@ -97,30 +103,18 @@ export function runFailedTestsReporterCli() { if (existingIssue) { const newFailureCount = await updateFailureIssue(buildUrl, existingIssue, githubApi); const url = existingIssue.html_url; - const message = - `Test has failed ${newFailureCount - 1} times on tracked branches: ${url}` + - (updateGithub - ? `. Updated existing issue: ${url} (fail count: ${newFailureCount})` - : ''); - - messages.push({ - classname: failure.classname, - name: failure.name, - message, - }); + pushMessage(`Test has failed ${newFailureCount - 1} times on tracked branches: ${url}`); + if (updateGithub) { + pushMessage(`Updated existing issue: ${url} (fail count: ${newFailureCount})`); + } continue; } const newIssueUrl = await createFailureIssue(buildUrl, failure, githubApi); - const message = - `Test has not failed recently on tracked branches` + - (updateGithub ? `Created new issue: ${newIssueUrl}` : ''); - - messages.push({ - classname: failure.classname, - name: failure.name, - message, - }); + pushMessage('Test has not failed recently on tracked branches'); + if (updateGithub) { + pushMessage(`Created new issue: ${newIssueUrl}`); + } } // mutates report to include messages and writes updated report to disk diff --git a/packages/kbn-test/src/failed_tests_reporter/test_report.ts b/packages/kbn-test/src/failed_tests_reporter/test_report.ts index 644a4cc9fd5a7..6b759ef1d4c62 100644 --- a/packages/kbn-test/src/failed_tests_reporter/test_report.ts +++ b/packages/kbn-test/src/failed_tests_reporter/test_report.ts @@ -58,13 +58,15 @@ export interface TestCase { classname: string; /* number of seconds this test took */ time: string; + /* optional JSON encoded metadata */ + 'metadata-json'?: string; }; /* contents of system-out elements */ - 'system-out'?: string[]; + 'system-out'?: Array; /* contents of failure elements */ failure?: Array; /* contents of skipped elements */ - skipped?: string[]; + skipped?: Array; } export interface FailedTestCase extends TestCase { @@ -82,19 +84,23 @@ export async function readTestReport(testReportPath: string) { return await parseTestReport(await readAsync(testReportPath, 'utf8')); } -export function* makeFailedTestCaseIter(report: TestReport) { - // Grab the failures. Reporters may report multiple testsuites in a single file. +export function* makeTestCaseIter(report: TestReport) { + // Reporters may report multiple testsuites in a single file. const testSuites = 'testsuites' in report ? report.testsuites.testsuite : [report.testsuite]; for (const testSuite of testSuites) { for (const testCase of testSuite.testcase) { - const { failure } = testCase; - - if (!failure) { - continue; - } + yield testCase; + } + } +} - yield testCase as FailedTestCase; +export function* makeFailedTestCaseIter(report: TestReport) { + for (const testCase of makeTestCaseIter(report)) { + if (!testCase.failure) { + continue; } + + yield testCase as FailedTestCase; } } diff --git a/packages/kbn-test/src/functional_test_runner/functional_test_runner.ts b/packages/kbn-test/src/functional_test_runner/functional_test_runner.ts index fcba9691b1772..e566a9a4af262 100644 --- a/packages/kbn-test/src/functional_test_runner/functional_test_runner.ts +++ b/packages/kbn-test/src/functional_test_runner/functional_test_runner.ts @@ -23,6 +23,7 @@ import { Suite, Test } from './fake_mocha_types'; import { Lifecycle, LifecyclePhase, + FailureMetadata, readConfigFile, ProviderCollection, readProviderSpec, @@ -33,6 +34,7 @@ import { export class FunctionalTestRunner { public readonly lifecycle = new Lifecycle(); + public readonly failureMetadata = new FailureMetadata(this.lifecycle); private closed = false; constructor( @@ -114,6 +116,7 @@ export class FunctionalTestRunner { const coreProviders = readProviderSpec('Service', { lifecycle: () => this.lifecycle, log: () => this.log, + failureMetadata: () => this.failureMetadata, config: () => config, }); diff --git a/packages/kbn-test/src/functional_test_runner/lib/failure_metadata.test.ts b/packages/kbn-test/src/functional_test_runner/lib/failure_metadata.test.ts new file mode 100644 index 0000000000000..7ae46ef6fac1e --- /dev/null +++ b/packages/kbn-test/src/functional_test_runner/lib/failure_metadata.test.ts @@ -0,0 +1,71 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Lifecycle } from './lifecycle'; +import { FailureMetadata } from './failure_metadata'; + +it('collects metadata for the current test', async () => { + const lifecycle = new Lifecycle(); + const failureMetadata = new FailureMetadata(lifecycle); + + const test1 = {}; + await lifecycle.beforeEachTest.trigger(test1); + failureMetadata.add({ foo: 'bar' }); + + expect(failureMetadata.get(test1)).toMatchInlineSnapshot(` + Object { + "foo": "bar", + } + `); + + const test2 = {}; + await lifecycle.beforeEachTest.trigger(test2); + failureMetadata.add({ test: 2 }); + + expect(failureMetadata.get(test1)).toMatchInlineSnapshot(` + Object { + "foo": "bar", + } + `); + expect(failureMetadata.get(test2)).toMatchInlineSnapshot(` + Object { + "test": 2, + } + `); +}); + +it('adds messages to the messages state', () => { + const lifecycle = new Lifecycle(); + const failureMetadata = new FailureMetadata(lifecycle); + + const test1 = {}; + lifecycle.beforeEachTest.trigger(test1); + failureMetadata.addMessages(['foo', 'bar']); + failureMetadata.addMessages(['baz']); + + expect(failureMetadata.get(test1)).toMatchInlineSnapshot(` + Object { + "messages": Array [ + "foo", + "bar", + "baz", + ], + } + `); +}); diff --git a/packages/kbn-test/src/functional_test_runner/lib/failure_metadata.ts b/packages/kbn-test/src/functional_test_runner/lib/failure_metadata.ts new file mode 100644 index 0000000000000..9dc58d5b0b21f --- /dev/null +++ b/packages/kbn-test/src/functional_test_runner/lib/failure_metadata.ts @@ -0,0 +1,104 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import Path from 'path'; + +import { REPO_ROOT } from '@kbn/dev-utils'; + +import { Lifecycle } from './lifecycle'; + +interface Metadata { + [key: string]: unknown; +} + +export class FailureMetadata { + // mocha's global types mean we can't import Mocha or it will override the global jest types.............. + private currentTest?: any; + private readonly allMetadata = new Map(); + + constructor(lifecycle: Lifecycle) { + if (!process.env.GCS_UPLOAD_PREFIX && process.env.CI) { + throw new Error( + 'GCS_UPLOAD_PREFIX environment variable is not set and must always be set on CI' + ); + } + + lifecycle.beforeEachTest.add(test => { + this.currentTest = test; + }); + } + + add(metadata: Metadata | ((current: Metadata) => Metadata)) { + if (!this.currentTest) { + throw new Error('no current test to associate metadata with'); + } + + const current = this.allMetadata.get(this.currentTest); + this.allMetadata.set(this.currentTest, { + ...current, + ...(typeof metadata === 'function' ? metadata(current || {}) : metadata), + }); + } + + addMessages(messages: string[]) { + this.add(current => ({ + messages: [...(Array.isArray(current.messages) ? current.messages : []), ...messages], + })); + } + + /** + * @param name Name to label the URL with + * @param repoPath absolute path, within the repo, that will be uploaded + */ + addScreenshot(name: string, repoPath: string) { + const prefix = process.env.GCS_UPLOAD_PREFIX; + + if (!prefix) { + return; + } + + const slash = prefix.endsWith('/') ? '' : '/'; + const urlPath = Path.relative(REPO_ROOT, repoPath) + .split(Path.sep) + .map(c => encodeURIComponent(c)) + .join('/'); + + if (urlPath.startsWith('..')) { + throw new Error( + `Only call addUploadLink() with paths that are within the repo root, received ${repoPath} and repo root is ${REPO_ROOT}` + ); + } + + const url = `https://storage.googleapis.com/${prefix}${slash}${urlPath}`; + const screenshot = { + name, + url, + }; + + this.add(current => ({ + screenshots: [...(Array.isArray(current.screenshots) ? current.screenshots : []), screenshot], + })); + + return screenshot; + } + + get(test: any) { + return this.allMetadata.get(test); + } +} diff --git a/packages/kbn-test/src/functional_test_runner/lib/index.ts b/packages/kbn-test/src/functional_test_runner/lib/index.ts index 2d354938d7648..8940eccad503a 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/index.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/index.ts @@ -22,3 +22,4 @@ export { LifecyclePhase } from './lifecycle_phase'; export { readConfigFile, Config } from './config'; export { readProviderSpec, ProviderCollection, Provider } from './providers'; export { runTests, setupMocha } from './mocha'; +export { FailureMetadata } from './failure_metadata'; diff --git a/packages/kbn-test/src/functional_test_runner/lib/lifecycle_event.ts b/packages/kbn-test/src/functional_test_runner/lib/lifecycle_event.ts new file mode 100644 index 0000000000000..22b7363454361 --- /dev/null +++ b/packages/kbn-test/src/functional_test_runner/lib/lifecycle_event.ts @@ -0,0 +1,68 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import * as Rx from 'rxjs'; + +export type GetArgsType> = T extends LifecycleEvent + ? X + : never; + +export class LifecycleEvent { + private readonly handlers: Array<(...args: Args) => Promise | void> = []; + + private readonly beforeSubj = this.options.singular + ? new Rx.BehaviorSubject(undefined) + : new Rx.Subject(); + public readonly before$ = this.beforeSubj.asObservable(); + + private readonly afterSubj = this.options.singular + ? new Rx.BehaviorSubject(undefined) + : new Rx.Subject(); + public readonly after$ = this.afterSubj.asObservable(); + + constructor( + private readonly options: { + singular?: boolean; + } = {} + ) {} + + public add(fn: (...args: Args) => Promise | void) { + this.handlers.push(fn); + } + + public async trigger(...args: Args) { + if (this.beforeSubj.isStopped) { + throw new Error(`singular lifecycle event can only be triggered once`); + } + + this.beforeSubj.next(undefined); + if (this.options.singular) { + this.beforeSubj.complete(); + } + + try { + await Promise.all(this.handlers.map(async fn => await fn(...args))); + } finally { + this.afterSubj.next(undefined); + if (this.options.singular) { + this.afterSubj.complete(); + } + } + } +} diff --git a/packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/reporter.js b/packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/reporter.js index ea697b096ce99..0e8c1bc121e15 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/reporter.js +++ b/packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/reporter.js @@ -32,6 +32,7 @@ import { writeEpilogue } from './write_epilogue'; export function MochaReporterProvider({ getService }) { const log = getService('log'); const config = getService('config'); + const failureMetadata = getService('failureMetadata'); let originalLogWriters; let reporterCaptureStartTime; @@ -53,6 +54,7 @@ export function MochaReporterProvider({ getService }) { if (config.get('junit.enabled') && config.get('junit.reportName')) { setupJUnitReportGeneration(runner, { reportName: config.get('junit.reportName'), + getTestMetadata: t => failureMetadata.get(t), }); } } diff --git a/packages/kbn-test/src/mocha/__tests__/junit_report_generation.js b/packages/kbn-test/src/mocha/__tests__/junit_report_generation.js index 1cb53ea0ca1c5..b27f2fcf47a5a 100644 --- a/packages/kbn-test/src/mocha/__tests__/junit_report_generation.js +++ b/packages/kbn-test/src/mocha/__tests__/junit_report_generation.js @@ -98,6 +98,7 @@ describe('dev/mocha/junit report generation', () => { classname: sharedClassname, name: 'SUITE works', time: testPass.$.time, + 'metadata-json': '{}', }, 'system-out': testPass['system-out'], }); @@ -109,6 +110,7 @@ describe('dev/mocha/junit report generation', () => { classname: sharedClassname, name: 'SUITE fails', time: testFail.$.time, + 'metadata-json': '{}', }, 'system-out': testFail['system-out'], failure: [testFail.failure[0]], @@ -124,6 +126,7 @@ describe('dev/mocha/junit report generation', () => { classname: sharedClassname, name: 'SUITE SUB_SUITE "before each" hook: fail hook for "never runs"', time: beforeEachFail.$.time, + 'metadata-json': '{}', }, 'system-out': testFail['system-out'], failure: [beforeEachFail.failure[0]], @@ -133,6 +136,7 @@ describe('dev/mocha/junit report generation', () => { $: { classname: sharedClassname, name: 'SUITE SUB_SUITE never runs', + 'metadata-json': '{}', }, 'system-out': testFail['system-out'], skipped: [''], diff --git a/packages/kbn-test/src/mocha/junit_report_generation.js b/packages/kbn-test/src/mocha/junit_report_generation.js index 51601fab12e53..80f63b2dc6595 100644 --- a/packages/kbn-test/src/mocha/junit_report_generation.js +++ b/packages/kbn-test/src/mocha/junit_report_generation.js @@ -32,6 +32,7 @@ export function setupJUnitReportGeneration(runner, options = {}) { const { reportName = 'Unnamed Mocha Tests', rootDirectory = dirname(require.resolve('../../../../package.json')), + getTestMetadata = () => ({}), } = options; const stats = {}; @@ -118,6 +119,7 @@ export function setupJUnitReportGeneration(runner, options = {}) { name: getFullTitle(node), classname: `${reportName}.${getPath(node).replace(/\./g, '·')}`, time: getDuration(node), + 'metadata-json': JSON.stringify(getTestMetadata(node) || {}), }); } diff --git a/packages/kbn-test/types/ftr.d.ts b/packages/kbn-test/types/ftr.d.ts index e917ed63ca5d3..8beecab88878d 100644 --- a/packages/kbn-test/types/ftr.d.ts +++ b/packages/kbn-test/types/ftr.d.ts @@ -18,9 +18,9 @@ */ import { ToolingLog } from '@kbn/dev-utils'; -import { Config, Lifecycle } from '../src/functional_test_runner/lib'; +import { Config, Lifecycle, FailureMetadata } from '../src/functional_test_runner/lib'; -export { Lifecycle, Config }; +export { Lifecycle, Config, FailureMetadata }; interface AsyncInstance { /** @@ -61,7 +61,7 @@ export interface GenericFtrProviderContext< * Determine if a service is avaliable * @param serviceName */ - hasService(serviceName: 'config' | 'log' | 'lifecycle'): true; + hasService(serviceName: 'config' | 'log' | 'lifecycle' | 'failureMetadata'): true; hasService(serviceName: K): serviceName is K; hasService(serviceName: string): serviceName is Extract; @@ -73,6 +73,7 @@ export interface GenericFtrProviderContext< getService(serviceName: 'config'): Config; getService(serviceName: 'log'): ToolingLog; getService(serviceName: 'lifecycle'): Lifecycle; + getService(serviceName: 'failureMetadata'): FailureMetadata; getService(serviceName: T): ServiceMap[T]; /** diff --git a/test/functional/services/screenshots.ts b/test/functional/services/screenshots.ts index ddafa211ece7f..9e673fe919a74 100644 --- a/test/functional/services/screenshots.ts +++ b/test/functional/services/screenshots.ts @@ -22,6 +22,7 @@ import { writeFile, readFileSync, mkdir } from 'fs'; import { promisify } from 'util'; import del from 'del'; + import { comparePngs } from './lib/compare_pngs'; import { FtrProviderContext } from '../ftr_provider_context'; import { WebElementWrapper } from './lib/web_element_wrapper'; @@ -32,6 +33,7 @@ const writeFileAsync = promisify(writeFile); export async function ScreenshotsProvider({ getService }: FtrProviderContext) { const log = getService('log'); const config = getService('config'); + const failureMetadata = getService('failureMetadata'); const browser = getService('browser'); const SESSION_DIRECTORY = resolve(config.get('screenshots.directory'), 'session'); @@ -68,11 +70,15 @@ export async function ScreenshotsProvider({ getService }: FtrProviderContext) { } async take(name: string, el?: WebElementWrapper) { - return await this._take(resolve(SESSION_DIRECTORY, `${name}.png`), el); + const path = resolve(SESSION_DIRECTORY, `${name}.png`); + await this._take(path, el); + failureMetadata.addScreenshot(name, path); } async takeForFailure(name: string, el?: WebElementWrapper) { - await this._take(resolve(FAILURE_DIRECTORY, `${name}.png`), el); + const path = resolve(FAILURE_DIRECTORY, `${name}.png`); + await this._take(path, el); + failureMetadata.addScreenshot(`failure[${name}]`, path); } async _take(path: string, el?: WebElementWrapper) { diff --git a/vars/kibanaPipeline.groovy b/vars/kibanaPipeline.groovy index 77907a07addd1..dbb33f2766dac 100644 --- a/vars/kibanaPipeline.groovy +++ b/vars/kibanaPipeline.groovy @@ -1,47 +1,45 @@ -def withWorkers(name, preWorkerClosure = {}, workerClosures = [:]) { +def withWorkers(machineName, preWorkerClosure = {}, workerClosures = [:]) { return { jobRunner('tests-xl', true) { - try { - doSetup() - preWorkerClosure() - - def nextWorker = 1 - def worker = { workerClosure -> - def workerNumber = nextWorker - nextWorker++ - - return { - // This delay helps smooth out CPU load caused by ES/Kibana instances starting up at the same time - def delay = (workerNumber-1)*20 - sleep(delay) - - workerClosure(workerNumber) + withGcsArtifactUpload(machineName, { + try { + doSetup() + preWorkerClosure() + + def nextWorker = 1 + def worker = { workerClosure -> + def workerNumber = nextWorker + nextWorker++ + + return { + // This delay helps smooth out CPU load caused by ES/Kibana instances starting up at the same time + def delay = (workerNumber-1)*20 + sleep(delay) + + workerClosure(workerNumber) + } } - } - def workers = [:] - workerClosures.each { workerName, workerClosure -> - workers[workerName] = worker(workerClosure) - } - - parallel(workers) - } finally { - catchError { - uploadAllGcsArtifacts(name) - } + def workers = [:] + workerClosures.each { workerName, workerClosure -> + workers[workerName] = worker(workerClosure) + } - catchError { - runErrorReporter() - } + parallel(workers) + } finally { + catchError { + runErrorReporter() + } - catchError { - runbld.junit() - } + catchError { + runbld.junit() + } - catchError { - publishJunit() + catchError { + publishJunit() + } } - } + }) } } } @@ -96,19 +94,19 @@ def legacyJobRunner(name) { "JOB=${name}", ]) { jobRunner('linux && immutable', false) { - try { - runbld('.ci/run.sh', "Execute ${name}", true) - } finally { - catchError { - uploadAllGcsArtifacts(name) - } - catchError { - runErrorReporter() + withGcsArtifactUpload(name, { + try { + runbld('.ci/run.sh', "Execute ${name}", true) + } finally { + catchError { + runErrorReporter() + } + + catchError { + publishJunit() + } } - catchError { - publishJunit() - } - } + }) } } } @@ -171,19 +169,18 @@ def jobRunner(label, useRamDisk, closure) { // TODO what should happen if GCS, Junit, or email publishing fails? Unstable build? Failed build? -def uploadGcsArtifact(workerName, pattern) { - def storageLocation = "gs://kibana-ci-artifacts/jobs/${env.JOB_NAME}/${BUILD_NUMBER}/${workerName}" // TODO - +def uploadGcsArtifact(uploadPrefix, pattern) { googleStorageUpload( credentialsId: 'kibana-ci-gcs-plugin', - bucket: storageLocation, + bucket: "gs://${uploadPrefix}", pattern: pattern, sharedPublicly: true, showInline: true, ) } -def uploadAllGcsArtifacts(workerName) { +def withGcsArtifactUpload(workerName, closure) { + def uploadPrefix = "kibana-ci-artifacts/jobs/${env.JOB_NAME}/${BUILD_NUMBER}/${workerName}" def ARTIFACT_PATTERNS = [ 'target/kibana-*', 'target/junit/**/*', @@ -194,9 +191,19 @@ def uploadAllGcsArtifacts(workerName) { 'x-pack/test/functional/apps/reporting/reports/session/*.pdf', ] - ARTIFACT_PATTERNS.each { pattern -> - uploadGcsArtifact(workerName, pattern) - } + withEnv([ + "GCS_UPLOAD_PREFIX=${uploadPrefix}" + ], { + try { + closure() + } finally { + catchError { + ARTIFACT_PATTERNS.each { pattern -> + uploadGcsArtifact(uploadPrefix, pattern) + } + } + } + }) } def publishJunit() { From ab1fe3f14e791957c865c7d6b5dff3d2ebf4a8dc Mon Sep 17 00:00:00 2001 From: Spencer Date: Wed, 11 Dec 2019 09:50:03 -0700 Subject: [PATCH 33/40] [kbnClient] Retry uiSettings.replace() calls up to 5 times (#52601) * [kbn/dev-utils] target ES2019 to transpile ?? * Retry uiSettings.replace() calls up to 5 times * share logic for selecting junit report name to ensure they are unique * convert to junit report path helper --- .../src/kbn_client/kbn_client_requester.ts | 70 ++++++++++--------- .../src/kbn_client/kbn_client_ui_settings.ts | 30 ++++---- packages/kbn-dev-utils/tsconfig.json | 1 + packages/kbn-test/src/index.ts | 2 + packages/kbn-test/src/junit_report_path.ts | 32 +++++++++ .../__tests__/junit_report_generation.js | 13 +--- .../src/mocha/junit_report_generation.js | 11 +-- .../integration_tests/junit_reporter.test.js | 3 +- src/dev/jest/junit_reporter.js | 10 +-- tasks/config/karma.js | 5 +- 10 files changed, 99 insertions(+), 78 deletions(-) create mode 100644 packages/kbn-test/src/junit_report_path.ts diff --git a/packages/kbn-dev-utils/src/kbn_client/kbn_client_requester.ts b/packages/kbn-dev-utils/src/kbn_client/kbn_client_requester.ts index 25962c91a896d..4244006f4a3a3 100644 --- a/packages/kbn-dev-utils/src/kbn_client/kbn_client_requester.ts +++ b/packages/kbn-dev-utils/src/kbn_client/kbn_client_requester.ts @@ -62,8 +62,7 @@ export interface ReqOptions { query?: Record; method: 'GET' | 'POST' | 'PUT' | 'DELETE'; body?: any; - attempt?: number; - maxAttempts?: number; + retries?: number; } const delay = (ms: number) => @@ -87,44 +86,47 @@ export class KbnClientRequester { async request(options: ReqOptions): Promise { const url = Url.resolve(this.pickUrl(), options.path); const description = options.description || `${options.method} ${url}`; - const attempt = options.attempt === undefined ? 1 : options.attempt; - const maxAttempts = - options.maxAttempts === undefined ? DEFAULT_MAX_ATTEMPTS : options.maxAttempts; - - try { - const response = await Axios.request({ - method: options.method, - url, - data: options.body, - params: options.query, - headers: { - 'kbn-xsrf': 'kbn-client', - }, - }); - - return response.data; - } catch (error) { - let retryErrorMsg: string | undefined; - if (isAxiosRequestError(error)) { - retryErrorMsg = `[${description}] request failed (attempt=${attempt})`; - } else if (isConcliftOnGetError(error)) { - retryErrorMsg = `Conflict on GET (path=${options.path}, attempt=${attempt})`; - } + let attempt = 0; + const maxAttempts = options.retries ?? DEFAULT_MAX_ATTEMPTS; + + while (true) { + attempt += 1; + + try { + const response = await Axios.request({ + method: options.method, + url, + data: options.body, + params: options.query, + headers: { + 'kbn-xsrf': 'kbn-client', + }, + }); + + return response.data; + } catch (error) { + const conflictOnGet = isConcliftOnGetError(error); + const requestedRetries = options.retries !== undefined; + const failedToGetResponse = isAxiosRequestError(error); + + let errorMessage; + if (conflictOnGet) { + errorMessage = `Conflict on GET (path=${options.path}, attempt=${attempt}/${maxAttempts})`; + this.log.error(errorMessage); + } else if (requestedRetries || failedToGetResponse) { + errorMessage = `[${description}] request failed (attempt=${attempt}/${maxAttempts})`; + this.log.error(errorMessage); + } else { + throw error; + } - if (retryErrorMsg) { if (attempt < maxAttempts) { - this.log.error(retryErrorMsg); await delay(1000 * attempt); - return await this.request({ - ...options, - attempt: attempt + 1, - }); + continue; } - throw new Error(retryErrorMsg + ' and ran out of retries'); + throw new Error(`${errorMessage} -- and ran out of retries`); } - - throw error; } } } diff --git a/packages/kbn-dev-utils/src/kbn_client/kbn_client_ui_settings.ts b/packages/kbn-dev-utils/src/kbn_client/kbn_client_ui_settings.ts index 03033bc5c2ccc..ad01dea624c3c 100644 --- a/packages/kbn-dev-utils/src/kbn_client/kbn_client_ui_settings.ts +++ b/packages/kbn-dev-utils/src/kbn_client/kbn_client_ui_settings.ts @@ -40,7 +40,7 @@ export class KbnClientUiSettings { async get(setting: string) { const all = await this.getAll(); - const value = all.settings[setting] ? all.settings[setting].userValue : undefined; + const value = all[setting]?.userValue; this.log.verbose('uiSettings.value: %j', value); return value; @@ -68,24 +68,24 @@ export class KbnClientUiSettings { * with some defaults */ async replace(doc: UiSettingValues) { - const all = await this.getAll(); - for (const [name, { isOverridden }] of Object.entries(all.settings)) { - if (!isOverridden) { - await this.unset(name); + this.log.debug('replacing kibana config doc: %j', doc); + + const changes: Record = { + ...this.defaults, + ...doc, + }; + + for (const [name, { isOverridden }] of Object.entries(await this.getAll())) { + if (!isOverridden && !changes.hasOwnProperty(name)) { + changes[name] = null; } } - this.log.debug('replacing kibana config doc: %j', doc); - await this.requester.request({ method: 'POST', path: '/api/kibana/settings', - body: { - changes: { - ...this.defaults, - ...doc, - }, - }, + body: { changes }, + retries: 5, }); } @@ -105,9 +105,11 @@ export class KbnClientUiSettings { } private async getAll() { - return await this.requester.request({ + const resp = await this.requester.request({ path: '/api/kibana/settings', method: 'GET', }); + + return resp.settings; } } diff --git a/packages/kbn-dev-utils/tsconfig.json b/packages/kbn-dev-utils/tsconfig.json index 40a3bd475f1c1..4c519a609d86f 100644 --- a/packages/kbn-dev-utils/tsconfig.json +++ b/packages/kbn-dev-utils/tsconfig.json @@ -2,6 +2,7 @@ "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "target", + "target": "ES2019", "declaration": true }, "include": [ diff --git a/packages/kbn-test/src/index.ts b/packages/kbn-test/src/index.ts index 62739fd37030f..06a83cdd8b1dc 100644 --- a/packages/kbn-test/src/index.ts +++ b/packages/kbn-test/src/index.ts @@ -49,3 +49,5 @@ export { } from './mocha'; export { runFailedTestsReporterCli } from './failed_tests_reporter'; + +export { makeJunitReportPath } from './junit_report_path'; diff --git a/packages/kbn-test/src/junit_report_path.ts b/packages/kbn-test/src/junit_report_path.ts new file mode 100644 index 0000000000000..11eaf3d2b14a5 --- /dev/null +++ b/packages/kbn-test/src/junit_report_path.ts @@ -0,0 +1,32 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { resolve } from 'path'; + +const job = process.env.JOB ? `job-${process.env.JOB}-` : ''; +const num = process.env.CI_WORKER_NUMBER ? `worker-${process.env.CI_WORKER_NUMBER}-` : ''; + +export function makeJunitReportPath(rootDirectory: string, reportName: string) { + return resolve( + rootDirectory, + 'target/junit', + process.env.JOB || '.', + `TEST-${job}${num}${reportName}.xml` + ); +} diff --git a/packages/kbn-test/src/mocha/__tests__/junit_report_generation.js b/packages/kbn-test/src/mocha/__tests__/junit_report_generation.js index b27f2fcf47a5a..7472e271bd1e9 100644 --- a/packages/kbn-test/src/mocha/__tests__/junit_report_generation.js +++ b/packages/kbn-test/src/mocha/__tests__/junit_report_generation.js @@ -25,6 +25,7 @@ import { parseString } from 'xml2js'; import del from 'del'; import Mocha from 'mocha'; import expect from '@kbn/expect'; +import { makeJunitReportPath } from '@kbn/test'; import { setupJUnitReportGeneration } from '../junit_report_generation'; @@ -50,17 +51,7 @@ describe('dev/mocha/junit report generation', () => { mocha.addFile(resolve(PROJECT_DIR, 'test.js')); await new Promise(resolve => mocha.run(resolve)); const report = await fcb(cb => - parseString( - readFileSync( - resolve( - PROJECT_DIR, - 'target/junit', - process.env.JOB || '.', - `TEST-${process.env.JOB ? process.env.JOB + '-' : ''}test.xml` - ) - ), - cb - ) + parseString(readFileSync(makeJunitReportPath(PROJECT_DIR, 'test')), cb) ); // test case results are wrapped in diff --git a/packages/kbn-test/src/mocha/junit_report_generation.js b/packages/kbn-test/src/mocha/junit_report_generation.js index 80f63b2dc6595..95e84117106a4 100644 --- a/packages/kbn-test/src/mocha/junit_report_generation.js +++ b/packages/kbn-test/src/mocha/junit_report_generation.js @@ -17,11 +17,12 @@ * under the License. */ -import { resolve, dirname, relative } from 'path'; +import { dirname, relative } from 'path'; import { writeFileSync, mkdirSync } from 'fs'; import { inspect } from 'util'; import xmlBuilder from 'xmlbuilder'; +import { makeJunitReportPath } from '@kbn/test'; import { getSnapshotOfRunnableLogs } from './log_cache'; import { escapeCdata } from '../'; @@ -137,13 +138,7 @@ export function setupJUnitReportGeneration(runner, options = {}) { } }); - const reportPath = resolve( - rootDirectory, - 'target/junit', - process.env.JOB || '.', - `TEST-${process.env.JOB ? process.env.JOB + '-' : ''}${reportName}.xml` - ); - + const reportPath = makeJunitReportPath(rootDirectory, reportName); const reportXML = builder.end(); mkdirSync(dirname(reportPath), { recursive: true }); writeFileSync(reportPath, reportXML, 'utf8'); diff --git a/src/dev/jest/integration_tests/junit_reporter.test.js b/src/dev/jest/integration_tests/junit_reporter.test.js index ed5d73cd87c40..2abfa5648dcca 100644 --- a/src/dev/jest/integration_tests/junit_reporter.test.js +++ b/src/dev/jest/integration_tests/junit_reporter.test.js @@ -24,12 +24,13 @@ import { readFileSync } from 'fs'; import del from 'del'; import execa from 'execa'; import xml2js from 'xml2js'; +import { makeJunitReportPath } from '@kbn/test'; const MINUTE = 1000 * 60; const ROOT_DIR = resolve(__dirname, '../../../../'); const FIXTURE_DIR = resolve(__dirname, '__fixtures__'); const TARGET_DIR = resolve(FIXTURE_DIR, 'target'); -const XML_PATH = resolve(TARGET_DIR, 'junit', process.env.JOB || '.', `TEST-${process.env.JOB ? process.env.JOB + '-' : ''}Jest Tests.xml`); +const XML_PATH = makeJunitReportPath(FIXTURE_DIR, 'Jest Tests'); afterAll(async () => { await del(TARGET_DIR); diff --git a/src/dev/jest/junit_reporter.js b/src/dev/jest/junit_reporter.js index 7f51326ee46bb..0f8003f4ed6a1 100644 --- a/src/dev/jest/junit_reporter.js +++ b/src/dev/jest/junit_reporter.js @@ -22,7 +22,7 @@ import { writeFileSync, mkdirSync } from 'fs'; import xmlBuilder from 'xmlbuilder'; -import { escapeCdata } from '@kbn/test'; +import { escapeCdata, makeJunitReportPath } from '@kbn/test'; const ROOT_DIR = dirname(require.resolve('../../../package.json')); @@ -102,13 +102,7 @@ export default class JestJUnitReporter { }); }); - const reportPath = resolve( - rootDirectory, - 'target/junit', - process.env.JOB || '.', - `TEST-${process.env.JOB ? process.env.JOB + '-' : ''}${reportName}.xml` - ); - + const reportPath = makeJunitReportPath(rootDirectory, reportName); const reportXML = root.end(); mkdirSync(dirname(reportPath), { recursive: true }); writeFileSync(reportPath, reportXML, 'utf8'); diff --git a/tasks/config/karma.js b/tasks/config/karma.js index 25723677390bd..23371e52dd9e1 100644 --- a/tasks/config/karma.js +++ b/tasks/config/karma.js @@ -17,8 +17,9 @@ * under the License. */ -import { resolve, dirname } from 'path'; +import { dirname } from 'path'; import { times } from 'lodash'; +import { makeJunitReportPath } from '@kbn/test'; const TOTAL_CI_SHARDS = 4; const ROOT = dirname(require.resolve('../../package.json')); @@ -79,7 +80,7 @@ module.exports = function (grunt) { reporters: pickReporters(), junitReporter: { - outputFile: resolve(ROOT, 'target/junit', process.env.JOB || '.', `TEST-${process.env.JOB ? process.env.JOB + '-' : ''}karma.xml`), + outputFile: makeJunitReportPath(ROOT, 'karma'), useBrowserName: false, nameFormatter: (_, result) => [...result.suite, result.description].join(' '), classNameFormatter: (_, result) => { From a91e53f18f692ad1f89f35cf289749da80f71773 Mon Sep 17 00:00:00 2001 From: Josh Dover Date: Wed, 11 Dec 2019 10:53:17 -0600 Subject: [PATCH 34/40] Add asResponse option to HttpService methods (#52434) --- .../public/kibana-plugin-public.httpbody.md | 12 - .../kibana-plugin-public.httperrorresponse.md | 2 +- ...ugin-public.httpfetchoptions.asresponse.md | 13 + .../kibana-plugin-public.httpfetchoptions.md | 1 + .../kibana-plugin-public.httphandler.md | 6 +- ...-plugin-public.httpinterceptor.response.md | 6 +- ...in-public.httpinterceptor.responseerror.md | 4 +- .../kibana-plugin-public.httpresponse.md | 19 -- ...bana-plugin-public.httpresponse.request.md | 11 - ...kibana-plugin-public.ihttpresponse.body.md | 13 + .../kibana-plugin-public.ihttpresponse.md | 21 ++ ...ana-plugin-public.ihttpresponse.request.md | 13 + ...na-plugin-public.ihttpresponse.response.md | 13 + ....ihttpresponseinterceptoroverrides.body.md | 13 + ...ublic.ihttpresponseinterceptoroverrides.md | 21 ++ ...tpresponseinterceptoroverrides.response.md | 13 + ...gin-public.interceptedhttpresponse.body.md | 11 - ...a-plugin-public.interceptedhttpresponse.md | 20 -- ...public.interceptedhttpresponse.response.md | 11 - .../core/public/kibana-plugin-public.md | 7 +- .../capabilities/capabilities_service.tsx | 2 +- src/core/public/http/fetch.ts | 155 ++++++++++++ src/core/public/http/http_service.test.ts | 30 ++- src/core/public/http/http_setup.ts | 232 +----------------- src/core/public/http/intercept.ts | 134 ++++++++++ src/core/public/http/response.ts | 40 +++ src/core/public/http/types.ts | 52 ++-- src/core/public/index.ts | 5 +- src/core/public/public.api.md | 40 +-- .../components/share_context_menu.test.tsx | 2 +- .../components/url_panel_content.test.tsx | 2 +- .../public/services/fetch_top_nodes.test.ts | 4 +- .../indexpattern_plugin/datapanel.test.tsx | 12 +- .../public/indexpattern_plugin/loader.test.ts | 3 +- .../session_timeout_http_interceptor.ts | 9 +- 35 files changed, 570 insertions(+), 382 deletions(-) delete mode 100644 docs/development/core/public/kibana-plugin-public.httpbody.md create mode 100644 docs/development/core/public/kibana-plugin-public.httpfetchoptions.asresponse.md delete mode 100644 docs/development/core/public/kibana-plugin-public.httpresponse.md delete mode 100644 docs/development/core/public/kibana-plugin-public.httpresponse.request.md create mode 100644 docs/development/core/public/kibana-plugin-public.ihttpresponse.body.md create mode 100644 docs/development/core/public/kibana-plugin-public.ihttpresponse.md create mode 100644 docs/development/core/public/kibana-plugin-public.ihttpresponse.request.md create mode 100644 docs/development/core/public/kibana-plugin-public.ihttpresponse.response.md create mode 100644 docs/development/core/public/kibana-plugin-public.ihttpresponseinterceptoroverrides.body.md create mode 100644 docs/development/core/public/kibana-plugin-public.ihttpresponseinterceptoroverrides.md create mode 100644 docs/development/core/public/kibana-plugin-public.ihttpresponseinterceptoroverrides.response.md delete mode 100644 docs/development/core/public/kibana-plugin-public.interceptedhttpresponse.body.md delete mode 100644 docs/development/core/public/kibana-plugin-public.interceptedhttpresponse.md delete mode 100644 docs/development/core/public/kibana-plugin-public.interceptedhttpresponse.response.md create mode 100644 src/core/public/http/fetch.ts create mode 100644 src/core/public/http/intercept.ts create mode 100644 src/core/public/http/response.ts diff --git a/docs/development/core/public/kibana-plugin-public.httpbody.md b/docs/development/core/public/kibana-plugin-public.httpbody.md deleted file mode 100644 index ab31f28b8dc38..0000000000000 --- a/docs/development/core/public/kibana-plugin-public.httpbody.md +++ /dev/null @@ -1,12 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpBody](./kibana-plugin-public.httpbody.md) - -## HttpBody type - - -Signature: - -```typescript -export declare type HttpBody = BodyInit | null | any; -``` diff --git a/docs/development/core/public/kibana-plugin-public.httperrorresponse.md b/docs/development/core/public/kibana-plugin-public.httperrorresponse.md index aa669df796a09..5b1ee898a444d 100644 --- a/docs/development/core/public/kibana-plugin-public.httperrorresponse.md +++ b/docs/development/core/public/kibana-plugin-public.httperrorresponse.md @@ -8,7 +8,7 @@ Signature: ```typescript -export interface HttpErrorResponse extends HttpResponse +export interface HttpErrorResponse extends IHttpResponse ``` ## Properties diff --git a/docs/development/core/public/kibana-plugin-public.httpfetchoptions.asresponse.md b/docs/development/core/public/kibana-plugin-public.httpfetchoptions.asresponse.md new file mode 100644 index 0000000000000..250cf83309b3c --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.httpfetchoptions.asresponse.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpFetchOptions](./kibana-plugin-public.httpfetchoptions.md) > [asResponse](./kibana-plugin-public.httpfetchoptions.asresponse.md) + +## HttpFetchOptions.asResponse property + +When `true` the return type of [HttpHandler](./kibana-plugin-public.httphandler.md) will be an [IHttpResponse](./kibana-plugin-public.ihttpresponse.md) with detailed request and response information. When `false`, the return type will just be the parsed response body. Defaults to `false`. + +Signature: + +```typescript +asResponse?: boolean; +``` diff --git a/docs/development/core/public/kibana-plugin-public.httpfetchoptions.md b/docs/development/core/public/kibana-plugin-public.httpfetchoptions.md index eca29b37425e9..6a0c4a8a7f137 100644 --- a/docs/development/core/public/kibana-plugin-public.httpfetchoptions.md +++ b/docs/development/core/public/kibana-plugin-public.httpfetchoptions.md @@ -16,6 +16,7 @@ export interface HttpFetchOptions extends HttpRequestInit | Property | Type | Description | | --- | --- | --- | +| [asResponse](./kibana-plugin-public.httpfetchoptions.asresponse.md) | boolean | When true the return type of [HttpHandler](./kibana-plugin-public.httphandler.md) will be an [IHttpResponse](./kibana-plugin-public.ihttpresponse.md) with detailed request and response information. When false, the return type will just be the parsed response body. Defaults to false. | | [headers](./kibana-plugin-public.httpfetchoptions.headers.md) | HttpHeadersInit | Headers to send with the request. See [HttpHeadersInit](./kibana-plugin-public.httpheadersinit.md). | | [prependBasePath](./kibana-plugin-public.httpfetchoptions.prependbasepath.md) | boolean | Whether or not the request should automatically prepend the basePath. Defaults to true. | | [query](./kibana-plugin-public.httpfetchoptions.query.md) | HttpFetchQuery | The query string for an HTTP request. See [HttpFetchQuery](./kibana-plugin-public.httpfetchquery.md). | diff --git a/docs/development/core/public/kibana-plugin-public.httphandler.md b/docs/development/core/public/kibana-plugin-public.httphandler.md index 80fd1ea2e5761..89458c4743cd6 100644 --- a/docs/development/core/public/kibana-plugin-public.httphandler.md +++ b/docs/development/core/public/kibana-plugin-public.httphandler.md @@ -2,12 +2,12 @@ [Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpHandler](./kibana-plugin-public.httphandler.md) -## HttpHandler type +## HttpHandler interface -A function for making an HTTP requests to Kibana's backend. See [HttpFetchOptions](./kibana-plugin-public.httpfetchoptions.md) for options and [HttpBody](./kibana-plugin-public.httpbody.md) for the response. +A function for making an HTTP requests to Kibana's backend. See [HttpFetchOptions](./kibana-plugin-public.httpfetchoptions.md) for options and [IHttpResponse](./kibana-plugin-public.ihttpresponse.md) for the response. Signature: ```typescript -export declare type HttpHandler = (path: string, options?: HttpFetchOptions) => Promise; +export interface HttpHandler ``` diff --git a/docs/development/core/public/kibana-plugin-public.httpinterceptor.response.md b/docs/development/core/public/kibana-plugin-public.httpinterceptor.response.md index ca43ea31f0e2e..3a67dcbad3119 100644 --- a/docs/development/core/public/kibana-plugin-public.httpinterceptor.response.md +++ b/docs/development/core/public/kibana-plugin-public.httpinterceptor.response.md @@ -9,17 +9,17 @@ Define an interceptor to be executed after a response is received. Signature: ```typescript -response?(httpResponse: HttpResponse, controller: IHttpInterceptController): Promise | InterceptedHttpResponse | void; +response?(httpResponse: IHttpResponse, controller: IHttpInterceptController): Promise | IHttpResponseInterceptorOverrides | void; ``` ## Parameters | Parameter | Type | Description | | --- | --- | --- | -| httpResponse | HttpResponse | | +| httpResponse | IHttpResponse | | | controller | IHttpInterceptController | | Returns: -`Promise | InterceptedHttpResponse | void` +`Promise | IHttpResponseInterceptorOverrides | void` diff --git a/docs/development/core/public/kibana-plugin-public.httpinterceptor.responseerror.md b/docs/development/core/public/kibana-plugin-public.httpinterceptor.responseerror.md index b8abd50e45461..476ceba649d40 100644 --- a/docs/development/core/public/kibana-plugin-public.httpinterceptor.responseerror.md +++ b/docs/development/core/public/kibana-plugin-public.httpinterceptor.responseerror.md @@ -9,7 +9,7 @@ Define an interceptor to be executed if a response interceptor throws an error o Signature: ```typescript -responseError?(httpErrorResponse: HttpErrorResponse, controller: IHttpInterceptController): Promise | InterceptedHttpResponse | void; +responseError?(httpErrorResponse: HttpErrorResponse, controller: IHttpInterceptController): Promise | IHttpResponseInterceptorOverrides | void; ``` ## Parameters @@ -21,5 +21,5 @@ responseError?(httpErrorResponse: HttpErrorResponse, controller: IHttpInterceptC Returns: -`Promise | InterceptedHttpResponse | void` +`Promise | IHttpResponseInterceptorOverrides | void` diff --git a/docs/development/core/public/kibana-plugin-public.httpresponse.md b/docs/development/core/public/kibana-plugin-public.httpresponse.md deleted file mode 100644 index e44515cc8a1e0..0000000000000 --- a/docs/development/core/public/kibana-plugin-public.httpresponse.md +++ /dev/null @@ -1,19 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpResponse](./kibana-plugin-public.httpresponse.md) - -## HttpResponse interface - - -Signature: - -```typescript -export interface HttpResponse extends InterceptedHttpResponse -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [request](./kibana-plugin-public.httpresponse.request.md) | Readonly<Request> | | - diff --git a/docs/development/core/public/kibana-plugin-public.httpresponse.request.md b/docs/development/core/public/kibana-plugin-public.httpresponse.request.md deleted file mode 100644 index 84ab1bc7af853..0000000000000 --- a/docs/development/core/public/kibana-plugin-public.httpresponse.request.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpResponse](./kibana-plugin-public.httpresponse.md) > [request](./kibana-plugin-public.httpresponse.request.md) - -## HttpResponse.request property - -Signature: - -```typescript -request: Readonly; -``` diff --git a/docs/development/core/public/kibana-plugin-public.ihttpresponse.body.md b/docs/development/core/public/kibana-plugin-public.ihttpresponse.body.md new file mode 100644 index 0000000000000..2f8710ccdc60e --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.ihttpresponse.body.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [IHttpResponse](./kibana-plugin-public.ihttpresponse.md) > [body](./kibana-plugin-public.ihttpresponse.body.md) + +## IHttpResponse.body property + +Parsed body received, may be undefined if there was an error. + +Signature: + +```typescript +readonly body?: TResponseBody; +``` diff --git a/docs/development/core/public/kibana-plugin-public.ihttpresponse.md b/docs/development/core/public/kibana-plugin-public.ihttpresponse.md new file mode 100644 index 0000000000000..5ddce0ba2d0f1 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.ihttpresponse.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [IHttpResponse](./kibana-plugin-public.ihttpresponse.md) + +## IHttpResponse interface + + +Signature: + +```typescript +export interface IHttpResponse +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [body](./kibana-plugin-public.ihttpresponse.body.md) | TResponseBody | Parsed body received, may be undefined if there was an error. | +| [request](./kibana-plugin-public.ihttpresponse.request.md) | Readonly<Request> | Raw request sent to Kibana server. | +| [response](./kibana-plugin-public.ihttpresponse.response.md) | Readonly<Response> | Raw response received, may be undefined if there was an error. | + diff --git a/docs/development/core/public/kibana-plugin-public.ihttpresponse.request.md b/docs/development/core/public/kibana-plugin-public.ihttpresponse.request.md new file mode 100644 index 0000000000000..12e5405eb5ed4 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.ihttpresponse.request.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [IHttpResponse](./kibana-plugin-public.ihttpresponse.md) > [request](./kibana-plugin-public.ihttpresponse.request.md) + +## IHttpResponse.request property + +Raw request sent to Kibana server. + +Signature: + +```typescript +readonly request: Readonly; +``` diff --git a/docs/development/core/public/kibana-plugin-public.ihttpresponse.response.md b/docs/development/core/public/kibana-plugin-public.ihttpresponse.response.md new file mode 100644 index 0000000000000..9d0b4b59a638d --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.ihttpresponse.response.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [IHttpResponse](./kibana-plugin-public.ihttpresponse.md) > [response](./kibana-plugin-public.ihttpresponse.response.md) + +## IHttpResponse.response property + +Raw response received, may be undefined if there was an error. + +Signature: + +```typescript +readonly response?: Readonly; +``` diff --git a/docs/development/core/public/kibana-plugin-public.ihttpresponseinterceptoroverrides.body.md b/docs/development/core/public/kibana-plugin-public.ihttpresponseinterceptoroverrides.body.md new file mode 100644 index 0000000000000..36fcfb390617c --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.ihttpresponseinterceptoroverrides.body.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [IHttpResponseInterceptorOverrides](./kibana-plugin-public.ihttpresponseinterceptoroverrides.md) > [body](./kibana-plugin-public.ihttpresponseinterceptoroverrides.body.md) + +## IHttpResponseInterceptorOverrides.body property + +Parsed body received, may be undefined if there was an error. + +Signature: + +```typescript +readonly body?: TResponseBody; +``` diff --git a/docs/development/core/public/kibana-plugin-public.ihttpresponseinterceptoroverrides.md b/docs/development/core/public/kibana-plugin-public.ihttpresponseinterceptoroverrides.md new file mode 100644 index 0000000000000..44f067c429e98 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.ihttpresponseinterceptoroverrides.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [IHttpResponseInterceptorOverrides](./kibana-plugin-public.ihttpresponseinterceptoroverrides.md) + +## IHttpResponseInterceptorOverrides interface + +Properties that can be returned by HttpInterceptor.request to override the response. + +Signature: + +```typescript +export interface IHttpResponseInterceptorOverrides +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [body](./kibana-plugin-public.ihttpresponseinterceptoroverrides.body.md) | TResponseBody | Parsed body received, may be undefined if there was an error. | +| [response](./kibana-plugin-public.ihttpresponseinterceptoroverrides.response.md) | Readonly<Response> | Raw response received, may be undefined if there was an error. | + diff --git a/docs/development/core/public/kibana-plugin-public.ihttpresponseinterceptoroverrides.response.md b/docs/development/core/public/kibana-plugin-public.ihttpresponseinterceptoroverrides.response.md new file mode 100644 index 0000000000000..bcba996645ba6 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.ihttpresponseinterceptoroverrides.response.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [IHttpResponseInterceptorOverrides](./kibana-plugin-public.ihttpresponseinterceptoroverrides.md) > [response](./kibana-plugin-public.ihttpresponseinterceptoroverrides.response.md) + +## IHttpResponseInterceptorOverrides.response property + +Raw response received, may be undefined if there was an error. + +Signature: + +```typescript +readonly response?: Readonly; +``` diff --git a/docs/development/core/public/kibana-plugin-public.interceptedhttpresponse.body.md b/docs/development/core/public/kibana-plugin-public.interceptedhttpresponse.body.md deleted file mode 100644 index fc6d34c0b74f2..0000000000000 --- a/docs/development/core/public/kibana-plugin-public.interceptedhttpresponse.body.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [InterceptedHttpResponse](./kibana-plugin-public.interceptedhttpresponse.md) > [body](./kibana-plugin-public.interceptedhttpresponse.body.md) - -## InterceptedHttpResponse.body property - -Signature: - -```typescript -body?: HttpBody; -``` diff --git a/docs/development/core/public/kibana-plugin-public.interceptedhttpresponse.md b/docs/development/core/public/kibana-plugin-public.interceptedhttpresponse.md deleted file mode 100644 index c4a7f4d6b2afa..0000000000000 --- a/docs/development/core/public/kibana-plugin-public.interceptedhttpresponse.md +++ /dev/null @@ -1,20 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [InterceptedHttpResponse](./kibana-plugin-public.interceptedhttpresponse.md) - -## InterceptedHttpResponse interface - - -Signature: - -```typescript -export interface InterceptedHttpResponse -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [body](./kibana-plugin-public.interceptedhttpresponse.body.md) | HttpBody | | -| [response](./kibana-plugin-public.interceptedhttpresponse.response.md) | Response | | - diff --git a/docs/development/core/public/kibana-plugin-public.interceptedhttpresponse.response.md b/docs/development/core/public/kibana-plugin-public.interceptedhttpresponse.response.md deleted file mode 100644 index dceb55113ee78..0000000000000 --- a/docs/development/core/public/kibana-plugin-public.interceptedhttpresponse.response.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [InterceptedHttpResponse](./kibana-plugin-public.interceptedhttpresponse.md) > [response](./kibana-plugin-public.interceptedhttpresponse.response.md) - -## InterceptedHttpResponse.response property - -Signature: - -```typescript -response?: Response; -``` diff --git a/docs/development/core/public/kibana-plugin-public.md b/docs/development/core/public/kibana-plugin-public.md index f527c92d070de..22b6f7faf2daa 100644 --- a/docs/development/core/public/kibana-plugin-public.md +++ b/docs/development/core/public/kibana-plugin-public.md @@ -52,10 +52,10 @@ The plugin integrates with the core system via lifecycle events: `setup` | [HttpErrorResponse](./kibana-plugin-public.httperrorresponse.md) | | | [HttpFetchOptions](./kibana-plugin-public.httpfetchoptions.md) | All options that may be used with a [HttpHandler](./kibana-plugin-public.httphandler.md). | | [HttpFetchQuery](./kibana-plugin-public.httpfetchquery.md) | | +| [HttpHandler](./kibana-plugin-public.httphandler.md) | A function for making an HTTP requests to Kibana's backend. See [HttpFetchOptions](./kibana-plugin-public.httpfetchoptions.md) for options and [IHttpResponse](./kibana-plugin-public.ihttpresponse.md) for the response. | | [HttpHeadersInit](./kibana-plugin-public.httpheadersinit.md) | | | [HttpInterceptor](./kibana-plugin-public.httpinterceptor.md) | An object that may define global interceptor functions for different parts of the request and response lifecycle. See [IHttpInterceptController](./kibana-plugin-public.ihttpinterceptcontroller.md). | | [HttpRequestInit](./kibana-plugin-public.httprequestinit.md) | Fetch API options available to [HttpHandler](./kibana-plugin-public.httphandler.md)s. | -| [HttpResponse](./kibana-plugin-public.httpresponse.md) | | | [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) | | | [I18nStart](./kibana-plugin-public.i18nstart.md) | I18nStart.Context is required by any localizable React component from @kbn/i18n and @elastic/eui packages and is supposed to be used as the topmost component for any i18n-compatible React tree. | | [IAnonymousPaths](./kibana-plugin-public.ianonymouspaths.md) | APIs for denoting paths as not requiring authentication | @@ -63,7 +63,8 @@ The plugin integrates with the core system via lifecycle events: `setup` | [IContextContainer](./kibana-plugin-public.icontextcontainer.md) | An object that handles registration of context providers and configuring handlers with context. | | [IHttpFetchError](./kibana-plugin-public.ihttpfetcherror.md) | | | [IHttpInterceptController](./kibana-plugin-public.ihttpinterceptcontroller.md) | Used to halt a request Promise chain in a [HttpInterceptor](./kibana-plugin-public.httpinterceptor.md). | -| [InterceptedHttpResponse](./kibana-plugin-public.interceptedhttpresponse.md) | | +| [IHttpResponse](./kibana-plugin-public.ihttpresponse.md) | | +| [IHttpResponseInterceptorOverrides](./kibana-plugin-public.ihttpresponseinterceptoroverrides.md) | Properties that can be returned by HttpInterceptor.request to override the response. | | [IUiSettingsClient](./kibana-plugin-public.iuisettingsclient.md) | Client-side client that provides access to the advanced settings stored in elasticsearch. The settings provide control over the behavior of the Kibana application. For example, a user can specify how to display numeric or date fields. Users can adjust the settings via Management UI. [IUiSettingsClient](./kibana-plugin-public.iuisettingsclient.md) | | [LegacyCoreSetup](./kibana-plugin-public.legacycoresetup.md) | Setup interface exposed to the legacy platform via the ui/new_platform module. | | [LegacyCoreStart](./kibana-plugin-public.legacycorestart.md) | Start interface exposed to the legacy platform via the ui/new_platform module. | @@ -108,8 +109,6 @@ The plugin integrates with the core system via lifecycle events: `setup` | [HandlerContextType](./kibana-plugin-public.handlercontexttype.md) | Extracts the type of the first argument of a [HandlerFunction](./kibana-plugin-public.handlerfunction.md) to represent the type of the context. | | [HandlerFunction](./kibana-plugin-public.handlerfunction.md) | A function that accepts a context object and an optional number of additional arguments. Used for the generic types in [IContextContainer](./kibana-plugin-public.icontextcontainer.md) | | [HandlerParameters](./kibana-plugin-public.handlerparameters.md) | Extracts the types of the additional arguments of a [HandlerFunction](./kibana-plugin-public.handlerfunction.md), excluding the [HandlerContextType](./kibana-plugin-public.handlercontexttype.md). | -| [HttpBody](./kibana-plugin-public.httpbody.md) | | -| [HttpHandler](./kibana-plugin-public.httphandler.md) | A function for making an HTTP requests to Kibana's backend. See [HttpFetchOptions](./kibana-plugin-public.httpfetchoptions.md) for options and [HttpBody](./kibana-plugin-public.httpbody.md) for the response. | | [HttpSetup](./kibana-plugin-public.httpsetup.md) | See [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) | | [HttpStart](./kibana-plugin-public.httpstart.md) | See [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) | | [IContextProvider](./kibana-plugin-public.icontextprovider.md) | A function that returns a context value for a specific key of given context type. | diff --git a/src/core/public/application/capabilities/capabilities_service.tsx b/src/core/public/application/capabilities/capabilities_service.tsx index e330a4b0326ae..24d9765953c44 100644 --- a/src/core/public/application/capabilities/capabilities_service.tsx +++ b/src/core/public/application/capabilities/capabilities_service.tsx @@ -74,7 +74,7 @@ export class CapabilitiesService { const url = http.anonymousPaths.isAnonymous(window.location.pathname) ? '/api/core/capabilities/defaults' : '/api/core/capabilities'; - const capabilities = await http.post(url, { + const capabilities = await http.post(url, { body: payload, }); return deepFreeze(capabilities); diff --git a/src/core/public/http/fetch.ts b/src/core/public/http/fetch.ts new file mode 100644 index 0000000000000..472b617cacd7f --- /dev/null +++ b/src/core/public/http/fetch.ts @@ -0,0 +1,155 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { merge } from 'lodash'; +import { format } from 'url'; + +import { IBasePath, HttpInterceptor, HttpHandler, HttpFetchOptions, IHttpResponse } from './types'; +import { HttpFetchError } from './http_fetch_error'; +import { HttpInterceptController } from './http_intercept_controller'; +import { HttpResponse } from './response'; +import { interceptRequest, interceptResponse } from './intercept'; +import { HttpInterceptHaltError } from './http_intercept_halt_error'; + +interface Params { + basePath: IBasePath; + kibanaVersion: string; +} + +const JSON_CONTENT = /^(application\/(json|x-javascript)|text\/(x-)?javascript|x-json)(;.*)?$/; +const NDJSON_CONTENT = /^(application\/ndjson)(;.*)?$/; + +export class FetchService { + private readonly interceptors = new Set(); + + constructor(private readonly params: Params) {} + + public intercept(interceptor: HttpInterceptor) { + this.interceptors.add(interceptor); + return () => this.interceptors.delete(interceptor); + } + + public removeAllInterceptors() { + this.interceptors.clear(); + } + + public fetch: HttpHandler = async ( + path: string, + options: HttpFetchOptions = {} + ) => { + const initialRequest = this.createRequest(path, options); + const controller = new HttpInterceptController(); + + // We wrap the interception in a separate promise to ensure that when + // a halt is called we do not resolve or reject, halting handling of the promise. + return new Promise>(async (resolve, reject) => { + try { + const interceptedRequest = await interceptRequest( + initialRequest, + this.interceptors, + controller + ); + const initialResponse = this.fetchResponse(interceptedRequest); + const interceptedResponse = await interceptResponse( + initialResponse, + this.interceptors, + controller + ); + + if (options.asResponse) { + resolve(interceptedResponse); + } else { + resolve(interceptedResponse.body); + } + } catch (error) { + if (!(error instanceof HttpInterceptHaltError)) { + reject(error); + } + } + }); + }; + + private createRequest(path: string, options?: HttpFetchOptions): Request { + // Merge and destructure options out that are not applicable to the Fetch API. + const { query, prependBasePath: shouldPrependBasePath, asResponse, ...fetchOptions } = merge( + { + method: 'GET', + credentials: 'same-origin', + prependBasePath: true, + headers: { + 'kbn-version': this.params.kibanaVersion, + 'Content-Type': 'application/json', + }, + }, + options || {} + ); + const url = format({ + pathname: shouldPrependBasePath ? this.params.basePath.prepend(path) : path, + query, + }); + + if ( + options && + options.headers && + 'Content-Type' in options.headers && + options.headers['Content-Type'] === undefined + ) { + delete fetchOptions.headers['Content-Type']; + } + + return new Request(url, fetchOptions); + } + + private async fetchResponse(request: Request) { + let response: Response; + let body = null; + + try { + response = await window.fetch(request); + } catch (err) { + throw new HttpFetchError(err.message, request); + } + + const contentType = response.headers.get('Content-Type') || ''; + + try { + if (NDJSON_CONTENT.test(contentType)) { + body = await response.blob(); + } else if (JSON_CONTENT.test(contentType)) { + body = await response.json(); + } else { + const text = await response.text(); + + try { + body = JSON.parse(text); + } catch (err) { + body = text; + } + } + } catch (err) { + throw new HttpFetchError(err.message, request, response, body); + } + + if (!response.ok) { + throw new HttpFetchError(response.statusText, request, response, body); + } + + return new HttpResponse({ request, response, body }); + } +} diff --git a/src/core/public/http/http_service.test.ts b/src/core/public/http/http_service.test.ts index 13906b91ed8df..09f3cca419e4d 100644 --- a/src/core/public/http/http_service.test.ts +++ b/src/core/public/http/http_service.test.ts @@ -24,7 +24,7 @@ import fetchMock from 'fetch-mock/es5/client'; import { readFileSync } from 'fs'; import { join } from 'path'; import { setup, SetupTap } from '../../../test_utils/public/http_test_setup'; -import { HttpResponse } from './types'; +import { IHttpResponse } from './types'; function delay(duration: number) { return new Promise(r => setTimeout(r, duration)); @@ -101,32 +101,32 @@ describe('http requests', () => { it('should return response', async () => { const { http } = setup(); - fetchMock.get('*', { foo: 'bar' }); - const json = await http.fetch('/my/path'); - expect(json).toEqual({ foo: 'bar' }); }); it('should prepend url with basepath by default', async () => { const { http } = setup(); - fetchMock.get('*', {}); await http.fetch('/my/path'); - expect(fetchMock.lastUrl()).toBe('http://localhost/myBase/my/path'); }); it('should not prepend url with basepath when disabled', async () => { const { http } = setup(); - fetchMock.get('*', {}); await http.fetch('my/path', { prependBasePath: false }); - expect(fetchMock.lastUrl()).toBe('/my/path'); }); + it('should not include undefined query params', async () => { + const { http } = setup(); + fetchMock.get('*', {}); + await http.fetch('/my/path', { query: { a: undefined } }); + expect(fetchMock.lastUrl()).toBe('http://localhost/myBase/my/path'); + }); + it('should make request with defaults', async () => { const { http } = setup(); @@ -145,6 +145,18 @@ describe('http requests', () => { }); }); + it('should expose detailed response object when asResponse = true', async () => { + const { http } = setup(); + + fetchMock.get('*', { foo: 'bar' }); + + const response = await http.fetch('/my/path', { asResponse: true }); + + expect(response.request).toBeInstanceOf(Request); + expect(response.response).toBeInstanceOf(Response); + expect(response.body).toEqual({ foo: 'bar' }); + }); + it('should reject on network error', async () => { const { http } = setup(); @@ -496,7 +508,7 @@ describe('interception', () => { it('should accumulate response information', async () => { const bodies = ['alpha', 'beta', 'gamma']; - const createResponse = jest.fn((httpResponse: HttpResponse) => ({ + const createResponse = jest.fn((httpResponse: IHttpResponse) => ({ body: bodies.shift(), })); diff --git a/src/core/public/http/http_setup.ts b/src/core/public/http/http_setup.ts index 602382e3a5a60..c63750849f13a 100644 --- a/src/core/public/http/http_setup.ts +++ b/src/core/public/http/http_setup.ts @@ -27,21 +27,16 @@ import { takeUntil, tap, } from 'rxjs/operators'; -import { merge } from 'lodash'; -import { format } from 'url'; import { InjectedMetadataSetup } from '../injected_metadata'; import { FatalErrorsSetup } from '../fatal_errors'; -import { HttpFetchOptions, HttpServiceBase, HttpInterceptor, HttpResponse } from './types'; +import { HttpFetchOptions, HttpServiceBase } from './types'; import { HttpInterceptController } from './http_intercept_controller'; -import { HttpFetchError } from './http_fetch_error'; import { HttpInterceptHaltError } from './http_intercept_halt_error'; import { BasePath } from './base_path_service'; import { AnonymousPaths } from './anonymous_paths'; +import { FetchService } from './fetch'; -const JSON_CONTENT = /^(application\/(json|x-javascript)|text\/(x-)?javascript|x-json)(;.*)?$/; -const NDJSON_CONTENT = /^(application\/ndjson)(;.*)?$/; - -function checkHalt(controller: HttpInterceptController, error?: Error) { +export function checkHalt(controller: HttpInterceptController, error?: Error) { if (error instanceof HttpInterceptHaltError) { throw error; } else if (controller.halted) { @@ -55,224 +50,15 @@ export const setup = ( ): HttpServiceBase => { const loadingCount$ = new BehaviorSubject(0); const stop$ = new Subject(); - const interceptors = new Set(); const kibanaVersion = injectedMetadata.getKibanaVersion(); const basePath = new BasePath(injectedMetadata.getBasePath()); const anonymousPaths = new AnonymousPaths(basePath); - function intercept(interceptor: HttpInterceptor) { - interceptors.add(interceptor); - - return () => interceptors.delete(interceptor); - } - - function removeAllInterceptors() { - interceptors.clear(); - } - - function createRequest(path: string, options?: HttpFetchOptions) { - const { query, prependBasePath: shouldPrependBasePath, ...fetchOptions } = merge( - { - method: 'GET', - credentials: 'same-origin', - prependBasePath: true, - headers: { - 'kbn-version': kibanaVersion, - 'Content-Type': 'application/json', - }, - }, - options || {} - ); - const url = format({ - pathname: shouldPrependBasePath ? basePath.prepend(path) : path, - query, - }); - - if ( - options && - options.headers && - 'Content-Type' in options.headers && - options.headers['Content-Type'] === undefined - ) { - delete fetchOptions.headers['Content-Type']; - } - - return new Request(url, fetchOptions); - } - - // Request/response interceptors are called in opposite orders. - // Request hooks start from the newest interceptor and end with the oldest. - function interceptRequest( - request: Request, - controller: HttpInterceptController - ): Promise { - let next = request; - - return [...interceptors].reduceRight( - (promise, interceptor) => - promise.then( - async (current: Request) => { - next = current; - checkHalt(controller); - - if (!interceptor.request) { - return current; - } - - return (await interceptor.request(current, controller)) || current; - }, - async error => { - checkHalt(controller, error); - - if (!interceptor.requestError) { - throw error; - } - - const nextRequest = await interceptor.requestError( - { error, request: next }, - controller - ); - - if (!nextRequest) { - throw error; - } - - next = nextRequest; - return next; - } - ), - Promise.resolve(request) - ); - } - - // Response hooks start from the oldest interceptor and end with the newest. - async function interceptResponse( - responsePromise: Promise, - controller: HttpInterceptController - ) { - let current: HttpResponse | undefined; - - const finalHttpResponse = await [...interceptors].reduce( - (promise, interceptor) => - promise.then( - async httpResponse => { - current = httpResponse; - checkHalt(controller); - - if (!interceptor.response) { - return httpResponse; - } - - return { - ...httpResponse, - ...((await interceptor.response(httpResponse, controller)) || {}), - }; - }, - async error => { - const request = error.request || (current && current.request); - - checkHalt(controller, error); - - if (!interceptor.responseError) { - throw error; - } - - try { - const next = await interceptor.responseError( - { - error, - request, - response: error.response || (current && current.response), - body: error.body || (current && current.body), - }, - controller - ); - - checkHalt(controller, error); - - if (!next) { - throw error; - } - - return { ...next, request }; - } catch (err) { - checkHalt(controller, err); - throw err; - } - } - ), - responsePromise - ); - - return finalHttpResponse.body; - } - - async function fetcher(request: Request): Promise { - let response; - let body = null; - - try { - response = await window.fetch(request); - } catch (err) { - throw new HttpFetchError(err.message, request); - } - - const contentType = response.headers.get('Content-Type') || ''; - - try { - if (NDJSON_CONTENT.test(contentType)) { - body = await response.blob(); - } else if (JSON_CONTENT.test(contentType)) { - body = await response.json(); - } else { - const text = await response.text(); - - try { - body = JSON.parse(text); - } catch (err) { - body = text; - } - } - } catch (err) { - throw new HttpFetchError(err.message, request, response, body); - } - - if (!response.ok) { - throw new HttpFetchError(response.statusText, request, response, body); - } - - return { response, body, request }; - } - - async function fetch(path: string, options: HttpFetchOptions = {}) { - const controller = new HttpInterceptController(); - const initialRequest = createRequest(path, options); - - // We wrap the interception in a separate promise to ensure that when - // a halt is called we do not resolve or reject, halting handling of the promise. - return new Promise(async (resolve, reject) => { - function rejectIfNotHalted(err: any) { - if (!(err instanceof HttpInterceptHaltError)) { - reject(err); - } - } - - try { - const request = await interceptRequest(initialRequest, controller); - - try { - resolve(await interceptResponse(fetcher(request), controller)); - } catch (err) { - rejectIfNotHalted(err); - } - } catch (err) { - rejectIfNotHalted(err); - } - }); - } + const fetchService = new FetchService({ basePath, kibanaVersion }); function shorthand(method: string) { - return (path: string, options: HttpFetchOptions = {}) => fetch(path, { ...options, method }); + return (path: string, options: HttpFetchOptions = {}) => + fetchService.fetch(path, { ...options, method }); } function stop() { @@ -321,9 +107,9 @@ export const setup = ( stop, basePath, anonymousPaths, - intercept, - removeAllInterceptors, - fetch, + intercept: fetchService.intercept.bind(fetchService), + removeAllInterceptors: fetchService.removeAllInterceptors.bind(fetchService), + fetch: fetchService.fetch.bind(fetchService), delete: shorthand('DELETE'), get: shorthand('GET'), head: shorthand('HEAD'), diff --git a/src/core/public/http/intercept.ts b/src/core/public/http/intercept.ts new file mode 100644 index 0000000000000..e2a16565c61c4 --- /dev/null +++ b/src/core/public/http/intercept.ts @@ -0,0 +1,134 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { HttpInterceptController } from './http_intercept_controller'; +import { HttpInterceptHaltError } from './http_intercept_halt_error'; +import { HttpInterceptor, IHttpResponse } from './types'; +import { HttpResponse } from './response'; + +export async function interceptRequest( + request: Request, + interceptors: ReadonlySet, + controller: HttpInterceptController +): Promise { + let next = request; + + return [...interceptors].reduceRight( + (promise, interceptor) => + promise.then( + async (current: Request) => { + next = current; + checkHalt(controller); + + if (!interceptor.request) { + return current; + } + + return (await interceptor.request(current, controller)) || current; + }, + async error => { + checkHalt(controller, error); + + if (!interceptor.requestError) { + throw error; + } + + const nextRequest = await interceptor.requestError({ error, request: next }, controller); + + if (!nextRequest) { + throw error; + } + + next = nextRequest; + return next; + } + ), + Promise.resolve(request) + ); +} + +export async function interceptResponse( + responsePromise: Promise, + interceptors: ReadonlySet, + controller: HttpInterceptController +): Promise { + let current: IHttpResponse; + + return await [...interceptors].reduce( + (promise, interceptor) => + promise.then( + async httpResponse => { + current = httpResponse; + checkHalt(controller); + + if (!interceptor.response) { + return httpResponse; + } + + const interceptorOverrides = (await interceptor.response(httpResponse, controller)) || {}; + + return new HttpResponse({ + ...httpResponse, + ...interceptorOverrides, + }); + }, + async error => { + const request = error.request || (current && current.request); + + checkHalt(controller, error); + + if (!interceptor.responseError) { + throw error; + } + + try { + const next = await interceptor.responseError( + { + error, + request, + response: error.response || (current && current.response), + body: error.body || (current && current.body), + }, + controller + ); + + checkHalt(controller, error); + + if (!next) { + throw error; + } + + return new HttpResponse({ ...next, request }); + } catch (err) { + checkHalt(controller, err); + throw err; + } + } + ), + responsePromise + ); +} + +function checkHalt(controller: HttpInterceptController, error?: Error) { + if (error instanceof HttpInterceptHaltError) { + throw error; + } else if (controller.halted) { + throw new HttpInterceptHaltError(); + } +} diff --git a/src/core/public/http/response.ts b/src/core/public/http/response.ts new file mode 100644 index 0000000000000..706e7caaca976 --- /dev/null +++ b/src/core/public/http/response.ts @@ -0,0 +1,40 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { IHttpResponse } from './types'; + +export class HttpResponse implements IHttpResponse { + public readonly request: Request; + public readonly response?: Response; + public readonly body?: TResponseBody; + + constructor({ + request, + response, + body, + }: { + request: Request; + response?: Response; + body?: TResponseBody; + }) { + this.request = request; + this.response = response; + this.body = body; + } +} diff --git a/src/core/public/http/types.ts b/src/core/public/http/types.ts index 870d4af8f9e86..48385a72325db 100644 --- a/src/core/public/http/types.ts +++ b/src/core/public/http/types.ts @@ -229,31 +229,49 @@ export interface HttpFetchOptions extends HttpRequestInit { * Headers to send with the request. See {@link HttpHeadersInit}. */ headers?: HttpHeadersInit; + + /** + * When `true` the return type of {@link HttpHandler} will be an {@link IHttpResponse} with detailed request and + * response information. When `false`, the return type will just be the parsed response body. Defaults to `false`. + */ + asResponse?: boolean; } /** * A function for making an HTTP requests to Kibana's backend. See {@link HttpFetchOptions} for options and - * {@link HttpBody} for the response. + * {@link IHttpResponse} for the response. * * @param path the path on the Kibana server to send the request to. Should not include the basePath. * @param options {@link HttpFetchOptions} - * @returns a Promise that resolves to a {@link HttpBody} + * @returns a Promise that resolves to a {@link IHttpResponse} * @public */ -export type HttpHandler = (path: string, options?: HttpFetchOptions) => Promise; - -/** @public */ -export type HttpBody = BodyInit | null | any; +export interface HttpHandler { + (path: string, options: HttpFetchOptions & { asResponse: true }): Promise< + IHttpResponse + >; + (path: string, options?: HttpFetchOptions): Promise; +} /** @public */ -export interface InterceptedHttpResponse { - response?: Response; - body?: HttpBody; +export interface IHttpResponse { + /** Raw request sent to Kibana server. */ + readonly request: Readonly; + /** Raw response received, may be undefined if there was an error. */ + readonly response?: Readonly; + /** Parsed body received, may be undefined if there was an error. */ + readonly body?: TResponseBody; } -/** @public */ -export interface HttpResponse extends InterceptedHttpResponse { - request: Readonly; +/** + * Properties that can be returned by HttpInterceptor.request to override the response. + * @public + */ +export interface IHttpResponseInterceptorOverrides { + /** Raw response received, may be undefined if there was an error. */ + readonly response?: Readonly; + /** Parsed body received, may be undefined if there was an error. */ + readonly body?: TResponseBody; } /** @public */ @@ -272,7 +290,7 @@ export interface IHttpFetchError extends Error { } /** @public */ -export interface HttpErrorResponse extends HttpResponse { +export interface HttpErrorResponse extends IHttpResponse { error: Error | IHttpFetchError; } /** @public */ @@ -310,13 +328,13 @@ export interface HttpInterceptor { /** * Define an interceptor to be executed after a response is received. - * @param httpResponse {@link HttpResponse} + * @param httpResponse {@link IHttpResponse} * @param controller {@link IHttpInterceptController} */ response?( - httpResponse: HttpResponse, + httpResponse: IHttpResponse, controller: IHttpInterceptController - ): Promise | InterceptedHttpResponse | void; + ): Promise | IHttpResponseInterceptorOverrides | void; /** * Define an interceptor to be executed if a response interceptor throws an error or returns a rejected Promise. @@ -326,7 +344,7 @@ export interface HttpInterceptor { responseError?( httpErrorResponse: HttpErrorResponse, controller: IHttpInterceptController - ): Promise | InterceptedHttpResponse | void; + ): Promise | IHttpResponseInterceptorOverrides | void; } /** diff --git a/src/core/public/index.ts b/src/core/public/index.ts index cfec03427f3e7..035cbcca86ac7 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -112,14 +112,13 @@ export { HttpErrorResponse, HttpErrorRequest, HttpInterceptor, - HttpResponse, + IHttpResponse, HttpHandler, - HttpBody, IBasePath, IAnonymousPaths, IHttpInterceptController, IHttpFetchError, - InterceptedHttpResponse, + IHttpResponseInterceptorOverrides, } from './http'; export { OverlayStart, OverlayBannersStart, OverlayRef } from './overlays'; diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 6fbc7324ce393..157f0bab466b0 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -464,9 +464,6 @@ export type HandlerFunction = (context: T, ...args: any[]) => // @public export type HandlerParameters> = T extends (context: any, ...args: infer U) => any ? U : never; -// @public (undocumented) -export type HttpBody = BodyInit | null | any; - // @public (undocumented) export interface HttpErrorRequest { // (undocumented) @@ -476,13 +473,14 @@ export interface HttpErrorRequest { } // @public (undocumented) -export interface HttpErrorResponse extends HttpResponse { +export interface HttpErrorResponse extends IHttpResponse { // (undocumented) error: Error | IHttpFetchError; } // @public export interface HttpFetchOptions extends HttpRequestInit { + asResponse?: boolean; headers?: HttpHeadersInit; prependBasePath?: boolean; query?: HttpFetchQuery; @@ -495,7 +493,14 @@ export interface HttpFetchQuery { } // @public -export type HttpHandler = (path: string, options?: HttpFetchOptions) => Promise; +export interface HttpHandler { + // (undocumented) + (path: string, options: HttpFetchOptions & { + asResponse: true; + }): Promise>; + // (undocumented) + (path: string, options?: HttpFetchOptions): Promise; +} // @public (undocumented) export interface HttpHeadersInit { @@ -507,8 +512,8 @@ export interface HttpHeadersInit { export interface HttpInterceptor { request?(request: Request, controller: IHttpInterceptController): Promise | Request | void; requestError?(httpErrorRequest: HttpErrorRequest, controller: IHttpInterceptController): Promise | Request | void; - response?(httpResponse: HttpResponse, controller: IHttpInterceptController): Promise | InterceptedHttpResponse | void; - responseError?(httpErrorResponse: HttpErrorResponse, controller: IHttpInterceptController): Promise | InterceptedHttpResponse | void; + response?(httpResponse: IHttpResponse, controller: IHttpInterceptController): Promise | IHttpResponseInterceptorOverrides | void; + responseError?(httpErrorResponse: HttpErrorResponse, controller: IHttpInterceptController): Promise | IHttpResponseInterceptorOverrides | void; } // @public @@ -529,12 +534,6 @@ export interface HttpRequestInit { window?: null; } -// @public (undocumented) -export interface HttpResponse extends InterceptedHttpResponse { - // (undocumented) - request: Readonly; -} - // @public (undocumented) export interface HttpServiceBase { addLoadingCount(countSource$: Observable): void; @@ -613,11 +612,16 @@ export interface IHttpInterceptController { } // @public (undocumented) -export interface InterceptedHttpResponse { - // (undocumented) - body?: HttpBody; - // (undocumented) - response?: Response; +export interface IHttpResponse { + readonly body?: TResponseBody; + readonly request: Readonly; + readonly response?: Readonly; +} + +// @public +export interface IHttpResponseInterceptorOverrides { + readonly body?: TResponseBody; + readonly response?: Readonly; } // @public diff --git a/src/plugins/share/public/components/share_context_menu.test.tsx b/src/plugins/share/public/components/share_context_menu.test.tsx index 7fb0449ead502..1f2242ae4c515 100644 --- a/src/plugins/share/public/components/share_context_menu.test.tsx +++ b/src/plugins/share/public/components/share_context_menu.test.tsx @@ -34,7 +34,7 @@ const defaultProps = { isDirty: false, onClose: () => {}, basePath: '', - post: () => Promise.resolve(), + post: () => Promise.resolve({} as any), objectType: 'dashboard', }; diff --git a/src/plugins/share/public/components/url_panel_content.test.tsx b/src/plugins/share/public/components/url_panel_content.test.tsx index 9da1a23641ab8..9db8d1ccf2efa 100644 --- a/src/plugins/share/public/components/url_panel_content.test.tsx +++ b/src/plugins/share/public/components/url_panel_content.test.tsx @@ -28,7 +28,7 @@ const defaultProps = { allowShortUrl: true, objectType: 'dashboard', basePath: '', - post: () => Promise.resolve(), + post: () => Promise.resolve({} as any), }; test('render', () => { diff --git a/x-pack/legacy/plugins/graph/public/services/fetch_top_nodes.test.ts b/x-pack/legacy/plugins/graph/public/services/fetch_top_nodes.test.ts index 0a0fc8cae5d26..3bfc868fcb06e 100644 --- a/x-pack/legacy/plugins/graph/public/services/fetch_top_nodes.test.ts +++ b/x-pack/legacy/plugins/graph/public/services/fetch_top_nodes.test.ts @@ -12,7 +12,7 @@ const icon = getSuitableIcon(''); describe('fetch_top_nodes', () => { it('should build terms agg', async () => { const postMock = jest.fn(() => Promise.resolve({ resp: {} })); - await fetchTopNodes(postMock, 'test', [ + await fetchTopNodes(postMock as any, 'test', [ { color: '', hopSize: 5, icon, name: 'field1', selected: false, type: 'string' }, { color: '', hopSize: 5, icon, name: 'field2', selected: false, type: 'string' }, ]); @@ -64,7 +64,7 @@ describe('fetch_top_nodes', () => { }, }) ); - const result = await fetchTopNodes(postMock, 'test', [ + const result = await fetchTopNodes(postMock as any, 'test', [ { color: 'red', hopSize: 5, icon, name: 'field1', selected: false, type: 'string' }, { color: 'blue', hopSize: 5, icon, name: 'field2', selected: false, type: 'string' }, ]); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.test.tsx index dc23df250ebd4..52f00a7cd4e9d 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.test.tsx @@ -278,7 +278,7 @@ describe('IndexPattern Data Panel', () => { function testProps() { const setState = jest.fn(); - core.http.get = jest.fn(async (url: string) => { + core.http.get.mockImplementation(async (url: string) => { const parts = url.split('/'); const indexPatternTitle = parts[parts.length - 1]; return { @@ -484,7 +484,7 @@ describe('IndexPattern Data Panel', () => { let overlapCount = 0; const props = testProps(); - core.http.get = jest.fn((url: string) => { + core.http.get.mockImplementation((url: string) => { if (queryCount) { ++overlapCount; } @@ -533,11 +533,9 @@ describe('IndexPattern Data Panel', () => { it('shows all fields if empty state button is clicked', async () => { const props = testProps(); - core.http.get = jest.fn((url: string) => { - return Promise.resolve({ - indexPatternTitle: props.currentIndexPatternId, - existingFieldNames: [], - }); + core.http.get.mockResolvedValue({ + indexPatternTitle: props.currentIndexPatternId, + existingFieldNames: [], }); const inst = mountWithIntl(); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/loader.test.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/loader.test.ts index 72cbd1b861a05..2fb678aed5a54 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/loader.test.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/loader.test.ts @@ -528,7 +528,8 @@ describe('loader', () => { await syncExistingFields({ dateRange: { fromDate: '1900-01-01', toDate: '2000-01-01' }, - fetchJson, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + fetchJson: fetchJson as any, indexPatterns: [{ title: 'a' }, { title: 'b' }, { title: 'c' }], setState, }); diff --git a/x-pack/plugins/security/public/session/session_timeout_http_interceptor.ts b/x-pack/plugins/security/public/session/session_timeout_http_interceptor.ts index 81625e1753b27..8a2251f3f7f7c 100644 --- a/x-pack/plugins/security/public/session/session_timeout_http_interceptor.ts +++ b/x-pack/plugins/security/public/session/session_timeout_http_interceptor.ts @@ -4,7 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { HttpInterceptor, HttpErrorResponse, HttpResponse, IAnonymousPaths } from 'src/core/public'; +import { + HttpInterceptor, + HttpErrorResponse, + IHttpResponse, + IAnonymousPaths, +} from 'src/core/public'; import { ISessionTimeout } from './session_timeout'; @@ -15,7 +20,7 @@ const isSystemAPIRequest = (request: Request) => { export class SessionTimeoutHttpInterceptor implements HttpInterceptor { constructor(private sessionTimeout: ISessionTimeout, private anonymousPaths: IAnonymousPaths) {} - response(httpResponse: HttpResponse) { + response(httpResponse: IHttpResponse) { if (this.anonymousPaths.isAnonymous(window.location.pathname)) { return; } From 2ec82d3dd918b58695cb5df4b90a420749832b16 Mon Sep 17 00:00:00 2001 From: Aleh Zasypkin Date: Wed, 11 Dec 2019 18:35:49 +0100 Subject: [PATCH 35/40] Migrate the rest of the API endpoints to the New Platform plugin (#50695) --- .../security/authentication/index.asciidoc | 2 +- test/common/services/security/user.ts | 4 +- .../setup-custom-kibana-user-role.ts | 6 +- .../common/{model/index.ts => model.ts} | 5 +- x-pack/legacy/plugins/security/index.js | 10 +- .../public/hacks/on_unauthorized_response.js | 4 +- .../legacy/plugins/security/public/lib/api.ts | 4 +- .../security/public/lib/api_keys_api.ts | 5 +- .../security/public/objects/lib/get_fields.ts | 2 +- .../public/services/shield_indices.js | 2 +- .../security/public/services/shield_user.js | 4 +- .../basic_login_form/basic_login_form.tsx | 2 +- .../security/public/views/logout/logout.js | 2 +- .../components/api_keys_grid_page.tsx | 2 +- .../invalidate_provider.tsx | 2 +- .../lib/__tests__/__fixtures__/request.ts | 39 -- .../lib/__tests__/__fixtures__/server.ts | 56 -- .../server/lib/route_pre_check_license.js | 18 - .../security/server/lib/user_schema.js | 17 - .../routes/api/v1/__tests__/authenticate.js | 260 -------- .../server/routes/api/v1/__tests__/users.js | 214 ------- .../server/routes/api/v1/api_keys/get.js | 45 -- .../server/routes/api/v1/api_keys/get.test.js | 166 ----- .../server/routes/api/v1/api_keys/index.js | 20 - .../routes/api/v1/api_keys/invalidate.js | 70 --- .../routes/api/v1/api_keys/invalidate.test.js | 200 ------ .../routes/api/v1/api_keys/privileges.js | 75 --- .../routes/api/v1/api_keys/privileges.test.js | 254 -------- .../server/routes/api/v1/authenticate.js | 227 ------- .../security/server/routes/api/v1/indices.js | 36 -- .../security/server/routes/api/v1/users.js | 142 ----- .../cypress/integration/lib/login/helpers.ts | 6 +- .../legacy/server/lib/esjs_shield_plugin.js | 579 ------------------ x-pack/legacy/server/lib/get_client_shield.ts | 14 - .../plugins/security/common/model/api_key.ts | 0 x-pack/plugins/security/common/model/index.ts | 1 + .../authentication/providers/oidc.test.ts | 15 +- .../server/elasticsearch_client_plugin.ts | 576 +++++++++++++++++ x-pack/plugins/security/server/errors.ts | 14 + x-pack/plugins/security/server/index.ts | 14 +- x-pack/plugins/security/server/plugin.test.ts | 9 +- x-pack/plugins/security/server/plugin.ts | 7 +- .../server/routes/api_keys/get.test.ts | 160 +++++ .../security/server/routes/api_keys/get.ts | 43 ++ .../security/server/routes/api_keys/index.ts | 16 + .../server/routes/api_keys/invalidate.test.ts | 220 +++++++ .../server/routes/api_keys/invalidate.ts | 69 +++ .../server/routes/api_keys/privileges.test.ts | 187 ++++++ .../server/routes/api_keys/privileges.ts | 49 ++ .../routes/authentication/basic.test.ts | 172 ++++++ .../server/routes/authentication/basic.ts | 48 ++ .../routes/authentication/common.test.ts | 202 ++++++ .../server/routes/authentication/common.ts | 78 +++ .../server/routes/authentication/index.ts | 28 + .../server/routes/authentication/oidc.ts | 274 +++++++++ .../server/routes/authentication/saml.ts | 19 +- .../authorization/privileges/get.test.ts | 2 +- .../routes/authorization/roles/delete.test.ts | 8 +- .../routes/authorization/roles/delete.ts | 8 +- .../routes/authorization/roles/get.test.ts | 8 +- .../server/routes/authorization/roles/get.ts | 8 +- .../authorization/roles/get_all.test.ts | 8 +- .../routes/authorization/roles/get_all.ts | 8 +- .../routes/authorization/roles/put.test.ts | 2 +- .../server/routes/authorization/roles/put.ts | 8 +- .../plugins/security/server/routes/index.ts | 6 + .../server/routes/indices/get_fields.ts | 47 ++ .../security/server/routes/indices/index.ts} | 7 +- .../routes/users/change_password.test.ts | 207 +++++++ .../server/routes/users/change_password.ts | 80 +++ .../server/routes/users/create_or_update.ts | 47 ++ .../security/server/routes/users/delete.ts | 32 + .../security/server/routes/users/get.ts | 37 ++ .../security/server/routes/users/get_all.ts | 27 + .../security/server/routes/users/index.ts | 20 + .../apis/security/basic_login.js | 36 +- .../apis/security/change_password.ts | 20 +- .../apis/security/index_fields.ts | 4 +- .../api_integration/apis/security/session.ts | 2 +- .../api_integration/services/legacy_es.js | 4 +- .../apis/security/kerberos_login.ts | 44 +- .../apis/authorization_code_flow/oidc_auth.js | 53 +- .../apis/implicit_flow/oidc_auth.ts | 20 +- x-pack/test/oidc_api_integration/config.ts | 4 +- .../apis/security/pki_auth.ts | 38 +- .../apis/security/saml_login.ts | 50 +- .../common/services/legacy_es.js | 4 +- .../common/services/legacy_es.js | 4 +- .../test/token_api_integration/auth/header.js | 10 +- .../test/token_api_integration/auth/login.js | 14 +- .../test/token_api_integration/auth/logout.js | 10 +- .../token_api_integration/auth/session.js | 16 +- 92 files changed, 2885 insertions(+), 2713 deletions(-) rename x-pack/legacy/plugins/security/common/{model/index.ts => model.ts} (84%) delete mode 100644 x-pack/legacy/plugins/security/server/lib/__tests__/__fixtures__/request.ts delete mode 100644 x-pack/legacy/plugins/security/server/lib/__tests__/__fixtures__/server.ts delete mode 100644 x-pack/legacy/plugins/security/server/lib/route_pre_check_license.js delete mode 100644 x-pack/legacy/plugins/security/server/lib/user_schema.js delete mode 100644 x-pack/legacy/plugins/security/server/routes/api/v1/__tests__/authenticate.js delete mode 100644 x-pack/legacy/plugins/security/server/routes/api/v1/__tests__/users.js delete mode 100644 x-pack/legacy/plugins/security/server/routes/api/v1/api_keys/get.js delete mode 100644 x-pack/legacy/plugins/security/server/routes/api/v1/api_keys/get.test.js delete mode 100644 x-pack/legacy/plugins/security/server/routes/api/v1/api_keys/index.js delete mode 100644 x-pack/legacy/plugins/security/server/routes/api/v1/api_keys/invalidate.js delete mode 100644 x-pack/legacy/plugins/security/server/routes/api/v1/api_keys/invalidate.test.js delete mode 100644 x-pack/legacy/plugins/security/server/routes/api/v1/api_keys/privileges.js delete mode 100644 x-pack/legacy/plugins/security/server/routes/api/v1/api_keys/privileges.test.js delete mode 100644 x-pack/legacy/plugins/security/server/routes/api/v1/authenticate.js delete mode 100644 x-pack/legacy/plugins/security/server/routes/api/v1/indices.js delete mode 100644 x-pack/legacy/plugins/security/server/routes/api/v1/users.js delete mode 100644 x-pack/legacy/server/lib/esjs_shield_plugin.js delete mode 100644 x-pack/legacy/server/lib/get_client_shield.ts rename x-pack/{legacy => }/plugins/security/common/model/api_key.ts (100%) create mode 100644 x-pack/plugins/security/server/elasticsearch_client_plugin.ts create mode 100644 x-pack/plugins/security/server/routes/api_keys/get.test.ts create mode 100644 x-pack/plugins/security/server/routes/api_keys/get.ts create mode 100644 x-pack/plugins/security/server/routes/api_keys/index.ts create mode 100644 x-pack/plugins/security/server/routes/api_keys/invalidate.test.ts create mode 100644 x-pack/plugins/security/server/routes/api_keys/invalidate.ts create mode 100644 x-pack/plugins/security/server/routes/api_keys/privileges.test.ts create mode 100644 x-pack/plugins/security/server/routes/api_keys/privileges.ts create mode 100644 x-pack/plugins/security/server/routes/authentication/basic.test.ts create mode 100644 x-pack/plugins/security/server/routes/authentication/basic.ts create mode 100644 x-pack/plugins/security/server/routes/authentication/common.test.ts create mode 100644 x-pack/plugins/security/server/routes/authentication/common.ts create mode 100644 x-pack/plugins/security/server/routes/authentication/oidc.ts create mode 100644 x-pack/plugins/security/server/routes/indices/get_fields.ts rename x-pack/{legacy/plugins/security/common/constants.ts => plugins/security/server/routes/indices/index.ts} (54%) create mode 100644 x-pack/plugins/security/server/routes/users/change_password.test.ts create mode 100644 x-pack/plugins/security/server/routes/users/change_password.ts create mode 100644 x-pack/plugins/security/server/routes/users/create_or_update.ts create mode 100644 x-pack/plugins/security/server/routes/users/delete.ts create mode 100644 x-pack/plugins/security/server/routes/users/get.ts create mode 100644 x-pack/plugins/security/server/routes/users/get_all.ts create mode 100644 x-pack/plugins/security/server/routes/users/index.ts diff --git a/docs/user/security/authentication/index.asciidoc b/docs/user/security/authentication/index.asciidoc index 32f341a9c1b7c..2e2aaf688e8b6 100644 --- a/docs/user/security/authentication/index.asciidoc +++ b/docs/user/security/authentication/index.asciidoc @@ -163,7 +163,7 @@ required by {kib}. If you want to use Third Party initiated SSO , then you must + [source,yaml] -------------------------------------------------------------------------------- -server.xsrf.whitelist: [/api/security/v1/oidc] +server.xsrf.whitelist: [/api/security/oidc/initiate_login] -------------------------------------------------------------------------------- [float] diff --git a/test/common/services/security/user.ts b/test/common/services/security/user.ts index e1c9b3fb998ad..ae02127043234 100644 --- a/test/common/services/security/user.ts +++ b/test/common/services/security/user.ts @@ -38,7 +38,7 @@ export class User { public async create(username: string, user: any) { this.log.debug(`creating user ${username}`); const { data, status, statusText } = await this.axios.post( - `/api/security/v1/users/${username}`, + `/internal/security/users/${username}`, { username, ...user, @@ -55,7 +55,7 @@ export class User { public async delete(username: string) { this.log.debug(`deleting user ${username}`); const { data, status, statusText } = await this.axios.delete( - `/api/security/v1/users/${username}` + `/internal/security/users/${username}` ); if (status !== 204) { throw new Error( diff --git a/x-pack/legacy/plugins/apm/scripts/kibana-security/setup-custom-kibana-user-role.ts b/x-pack/legacy/plugins/apm/scripts/kibana-security/setup-custom-kibana-user-role.ts index 9591dfdecbfef..66f2a8d1ac79f 100644 --- a/x-pack/legacy/plugins/apm/scripts/kibana-security/setup-custom-kibana-user-role.ts +++ b/x-pack/legacy/plugins/apm/scripts/kibana-security/setup-custom-kibana-user-role.ts @@ -183,7 +183,7 @@ async function createOrUpdateUser(newUser: User) { async function createUser(newUser: User) { const user = await callKibana({ method: 'POST', - url: `/api/security/v1/users/${newUser.username}`, + url: `/internal/security/users/${newUser.username}`, data: { ...newUser, enabled: true, @@ -209,7 +209,7 @@ async function updateUser(existingUser: User, newUser: User) { // assign role to user await callKibana({ method: 'POST', - url: `/api/security/v1/users/${username}`, + url: `/internal/security/users/${username}`, data: { ...existingUser, roles: allRoles } }); @@ -219,7 +219,7 @@ async function updateUser(existingUser: User, newUser: User) { async function getUser(username: string) { try { return await callKibana({ - url: `/api/security/v1/users/${username}` + url: `/internal/security/users/${username}` }); } catch (e) { const err = e as AxiosError; diff --git a/x-pack/legacy/plugins/security/common/model/index.ts b/x-pack/legacy/plugins/security/common/model.ts similarity index 84% rename from x-pack/legacy/plugins/security/common/model/index.ts rename to x-pack/legacy/plugins/security/common/model.ts index 6c2976815559b..90e6a5403dfe8 100644 --- a/x-pack/legacy/plugins/security/common/model/index.ts +++ b/x-pack/legacy/plugins/security/common/model.ts @@ -4,8 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -export { ApiKey } from './api_key'; export { + ApiKey, + ApiKeyToInvalidate, AuthenticatedUser, BuiltinESPrivileges, EditUser, @@ -19,4 +20,4 @@ export { User, canUserChangePassword, getUserDisplayName, -} from '../../../../../plugins/security/common/model'; +} from '../../../../plugins/security/common/model'; diff --git a/x-pack/legacy/plugins/security/index.js b/x-pack/legacy/plugins/security/index.js index 115dd8b9b8206..3a6f3692bc0b6 100644 --- a/x-pack/legacy/plugins/security/index.js +++ b/x-pack/legacy/plugins/security/index.js @@ -5,10 +5,6 @@ */ import { resolve } from 'path'; -import { initAuthenticateApi } from './server/routes/api/v1/authenticate'; -import { initUsersApi } from './server/routes/api/v1/users'; -import { initApiKeysApi } from './server/routes/api/v1/api_keys'; -import { initIndicesApi } from './server/routes/api/v1/indices'; import { initOverwrittenSessionView } from './server/routes/views/overwritten_session'; import { initLoginView } from './server/routes/views/login'; import { initLogoutView } from './server/routes/views/logout'; @@ -34,7 +30,7 @@ export const security = (kibana) => new kibana.Plugin({ lifespan: Joi.any().description('This key is handled in the new platform security plugin ONLY'), }).default(), secureCookies: Joi.any().description('This key is handled in the new platform security plugin ONLY'), - loginAssistanceMessage: Joi.string().default(), + loginAssistanceMessage: Joi.any().description('This key is handled in the new platform security plugin ONLY'), authorization: Joi.object({ legacyFallback: Joi.object({ enabled: Joi.boolean().default(true) // deprecated @@ -144,10 +140,6 @@ export const security = (kibana) => new kibana.Plugin({ server.expose({ getUser: request => securityPlugin.authc.getCurrentUser(KibanaRequest.from(request)) }); - initAuthenticateApi(securityPlugin, server); - initUsersApi(securityPlugin, server); - initApiKeysApi(server); - initIndicesApi(server); initLoginView(securityPlugin, server); initLogoutView(server); initLoggedOutView(securityPlugin, server); diff --git a/x-pack/legacy/plugins/security/public/hacks/on_unauthorized_response.js b/x-pack/legacy/plugins/security/public/hacks/on_unauthorized_response.js index 6d03f3da6e2f2..efc227e2c2789 100644 --- a/x-pack/legacy/plugins/security/public/hacks/on_unauthorized_response.js +++ b/x-pack/legacy/plugins/security/public/hacks/on_unauthorized_response.js @@ -11,8 +11,8 @@ import 'plugins/security/services/auto_logout'; function isUnauthorizedResponseAllowed(response) { const API_WHITELIST = [ - '/api/security/v1/login', - '/api/security/v1/users/.*/password' + '/internal/security/login', + '/internal/security/users/.*/password' ]; const url = response.config.url; diff --git a/x-pack/legacy/plugins/security/public/lib/api.ts b/x-pack/legacy/plugins/security/public/lib/api.ts index e6e42ed5bd4da..ffa08ca44f376 100644 --- a/x-pack/legacy/plugins/security/public/lib/api.ts +++ b/x-pack/legacy/plugins/security/public/lib/api.ts @@ -7,12 +7,12 @@ import { kfetch } from 'ui/kfetch'; import { AuthenticatedUser, Role, User, EditUser } from '../../common/model'; -const usersUrl = '/api/security/v1/users'; +const usersUrl = '/internal/security/users'; const rolesUrl = '/api/security/role'; export class UserAPIClient { public async getCurrentUser(): Promise { - return await kfetch({ pathname: `/api/security/v1/me` }); + return await kfetch({ pathname: `/internal/security/me` }); } public async getUsers(): Promise { diff --git a/x-pack/legacy/plugins/security/public/lib/api_keys_api.ts b/x-pack/legacy/plugins/security/public/lib/api_keys_api.ts index c6dcef392af98..fbc0460c5908a 100644 --- a/x-pack/legacy/plugins/security/public/lib/api_keys_api.ts +++ b/x-pack/legacy/plugins/security/public/lib/api_keys_api.ts @@ -5,8 +5,7 @@ */ import { kfetch } from 'ui/kfetch'; -import { ApiKey, ApiKeyToInvalidate } from '../../common/model/api_key'; -import { INTERNAL_API_BASE_PATH } from '../../common/constants'; +import { ApiKey, ApiKeyToInvalidate } from '../../common/model'; interface CheckPrivilegesResponse { areApiKeysEnabled: boolean; @@ -22,7 +21,7 @@ interface GetApiKeysResponse { apiKeys: ApiKey[]; } -const apiKeysUrl = `${INTERNAL_API_BASE_PATH}/api_key`; +const apiKeysUrl = `/internal/security/api_key`; export class ApiKeysApi { public static async checkPrivileges(): Promise { diff --git a/x-pack/legacy/plugins/security/public/objects/lib/get_fields.ts b/x-pack/legacy/plugins/security/public/objects/lib/get_fields.ts index e0998eb8b8f6b..91d98782dab42 100644 --- a/x-pack/legacy/plugins/security/public/objects/lib/get_fields.ts +++ b/x-pack/legacy/plugins/security/public/objects/lib/get_fields.ts @@ -6,7 +6,7 @@ import { IHttpResponse } from 'angular'; import chrome from 'ui/chrome'; -const apiBase = chrome.addBasePath(`/api/security/v1/fields`); +const apiBase = chrome.addBasePath(`/internal/security/fields`); export async function getFields($http: any, query: string): Promise { return await $http diff --git a/x-pack/legacy/plugins/security/public/services/shield_indices.js b/x-pack/legacy/plugins/security/public/services/shield_indices.js index 2e25d73acbcee..973569eb6e9c3 100644 --- a/x-pack/legacy/plugins/security/public/services/shield_indices.js +++ b/x-pack/legacy/plugins/security/public/services/shield_indices.js @@ -10,7 +10,7 @@ const module = uiModules.get('security', []); module.service('shieldIndices', ($http, chrome) => { return { getFields: (query) => { - return $http.get(chrome.addBasePath(`/api/security/v1/fields/${query}`)) + return $http.get(chrome.addBasePath(`/internal/security/fields/${query}`)) .then(response => response.data); } }; diff --git a/x-pack/legacy/plugins/security/public/services/shield_user.js b/x-pack/legacy/plugins/security/public/services/shield_user.js index e77895caaa2ba..53252e851e353 100644 --- a/x-pack/legacy/plugins/security/public/services/shield_user.js +++ b/x-pack/legacy/plugins/security/public/services/shield_user.js @@ -10,7 +10,7 @@ import { uiModules } from 'ui/modules'; const module = uiModules.get('security', ['ngResource']); module.service('ShieldUser', ($resource, chrome) => { - const baseUrl = chrome.addBasePath('/api/security/v1/users/:username'); + const baseUrl = chrome.addBasePath('/internal/security/users/:username'); const ShieldUser = $resource(baseUrl, { username: '@username' }, { @@ -21,7 +21,7 @@ module.service('ShieldUser', ($resource, chrome) => { }, getCurrent: { method: 'GET', - url: chrome.addBasePath('/api/security/v1/me') + url: chrome.addBasePath('/internal/security/me') } }); diff --git a/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/basic_login_form.tsx b/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/basic_login_form.tsx index acdc29842d4c6..e6d3b5b7536b6 100644 --- a/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/basic_login_form.tsx +++ b/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/basic_login_form.tsx @@ -190,7 +190,7 @@ class BasicLoginFormUI extends Component { const { username, password } = this.state; - http.post('./api/security/v1/login', { username, password }).then( + http.post('./internal/security/login', { username, password }).then( () => (window.location.href = next), (error: any) => { const { statusCode = 500 } = error.data || {}; diff --git a/x-pack/legacy/plugins/security/public/views/logout/logout.js b/x-pack/legacy/plugins/security/public/views/logout/logout.js index 4411ecdade8e7..5d76dfc2908c8 100644 --- a/x-pack/legacy/plugins/security/public/views/logout/logout.js +++ b/x-pack/legacy/plugins/security/public/views/logout/logout.js @@ -12,5 +12,5 @@ chrome $window.sessionStorage.clear(); // Redirect user to the server logout endpoint to complete logout. - $window.location.href = chrome.addBasePath(`/api/security/v1/logout${$window.location.search}`); + $window.location.href = chrome.addBasePath(`/api/security/logout${$window.location.search}`); }); diff --git a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/api_keys_grid_page.tsx b/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/api_keys_grid_page.tsx index 37838cfdb950d..1613e3804c31d 100644 --- a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/api_keys_grid_page.tsx +++ b/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/api_keys_grid_page.tsx @@ -29,7 +29,7 @@ import _ from 'lodash'; import { toastNotifications } from 'ui/notify'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { SectionLoading } from '../../../../../../../../../src/plugins/es_ui_shared/public/components/section_loading'; -import { ApiKey, ApiKeyToInvalidate } from '../../../../../common/model/api_key'; +import { ApiKey, ApiKeyToInvalidate } from '../../../../../common/model'; import { ApiKeysApi } from '../../../../lib/api_keys_api'; import { PermissionDenied } from './permission_denied'; import { EmptyPrompt } from './empty_prompt'; diff --git a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/invalidate_provider/invalidate_provider.tsx b/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/invalidate_provider/invalidate_provider.tsx index fe9ffc651db29..a1627442b89b8 100644 --- a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/invalidate_provider/invalidate_provider.tsx +++ b/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/invalidate_provider/invalidate_provider.tsx @@ -8,7 +8,7 @@ import React, { Fragment, useRef, useState } from 'react'; import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; import { toastNotifications } from 'ui/notify'; import { i18n } from '@kbn/i18n'; -import { ApiKeyToInvalidate } from '../../../../../../common/model/api_key'; +import { ApiKeyToInvalidate } from '../../../../../../common/model'; import { ApiKeysApi } from '../../../../../lib/api_keys_api'; interface Props { diff --git a/x-pack/legacy/plugins/security/server/lib/__tests__/__fixtures__/request.ts b/x-pack/legacy/plugins/security/server/lib/__tests__/__fixtures__/request.ts deleted file mode 100644 index c928a38d88ef3..0000000000000 --- a/x-pack/legacy/plugins/security/server/lib/__tests__/__fixtures__/request.ts +++ /dev/null @@ -1,39 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { Request } from 'hapi'; -import url from 'url'; - -interface RequestFixtureOptions { - headers?: Record; - auth?: string; - params?: Record; - path?: string; - basePath?: string; - search?: string; - payload?: unknown; -} - -export function requestFixture({ - headers = { accept: 'something/html' }, - auth, - params, - path = '/wat', - search = '', - payload, -}: RequestFixtureOptions = {}) { - return ({ - raw: { req: { headers } }, - auth, - headers, - params, - url: { path, search }, - query: search ? url.parse(search, true /* parseQueryString */).query : {}, - payload, - state: { user: 'these are the contents of the user client cookie' }, - route: { settings: {} }, - } as any) as Request; -} diff --git a/x-pack/legacy/plugins/security/server/lib/__tests__/__fixtures__/server.ts b/x-pack/legacy/plugins/security/server/lib/__tests__/__fixtures__/server.ts deleted file mode 100644 index 55b6f735cfced..0000000000000 --- a/x-pack/legacy/plugins/security/server/lib/__tests__/__fixtures__/server.ts +++ /dev/null @@ -1,56 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { stub } from 'sinon'; - -export function serverFixture() { - return { - config: stub(), - register: stub(), - expose: stub(), - log: stub(), - route: stub(), - decorate: stub(), - - info: { - protocol: 'protocol', - }, - - auth: { - strategy: stub(), - test: stub(), - }, - - plugins: { - elasticsearch: { - createCluster: stub(), - }, - - kibana: { - systemApi: { isSystemApiRequest: stub() }, - }, - - security: { - getUser: stub(), - authenticate: stub(), - deauthenticate: stub(), - authorization: { - application: stub(), - }, - }, - - xpack_main: { - info: { - isAvailable: stub(), - feature: stub(), - license: { - isOneOf: stub(), - }, - }, - }, - }, - }; -} diff --git a/x-pack/legacy/plugins/security/server/lib/route_pre_check_license.js b/x-pack/legacy/plugins/security/server/lib/route_pre_check_license.js deleted file mode 100644 index 64816bf4d23d7..0000000000000 --- a/x-pack/legacy/plugins/security/server/lib/route_pre_check_license.js +++ /dev/null @@ -1,18 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -const Boom = require('boom'); - -export function routePreCheckLicense(server) { - return function forbidApiAccess() { - const licenseCheckResults = server.newPlatform.setup.plugins.security.__legacyCompat.license.getFeatures(); - if (!licenseCheckResults.showLinks) { - throw Boom.forbidden(licenseCheckResults.linksMessage); - } else { - return null; - } - }; -} diff --git a/x-pack/legacy/plugins/security/server/lib/user_schema.js b/x-pack/legacy/plugins/security/server/lib/user_schema.js deleted file mode 100644 index 57c66b2712025..0000000000000 --- a/x-pack/legacy/plugins/security/server/lib/user_schema.js +++ /dev/null @@ -1,17 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import Joi from 'joi'; - -export const userSchema = Joi.object({ - username: Joi.string().required(), - password: Joi.string(), - roles: Joi.array().items(Joi.string()), - full_name: Joi.string().allow(null, ''), - email: Joi.string().allow(null, ''), - metadata: Joi.object(), - enabled: Joi.boolean().default(true) -}); diff --git a/x-pack/legacy/plugins/security/server/routes/api/v1/__tests__/authenticate.js b/x-pack/legacy/plugins/security/server/routes/api/v1/__tests__/authenticate.js deleted file mode 100644 index 5cea7c70b7781..0000000000000 --- a/x-pack/legacy/plugins/security/server/routes/api/v1/__tests__/authenticate.js +++ /dev/null @@ -1,260 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from '@kbn/expect'; -import Boom from 'boom'; -import Joi from 'joi'; -import sinon from 'sinon'; - -import { serverFixture } from '../../../../lib/__tests__/__fixtures__/server'; -import { requestFixture } from '../../../../lib/__tests__/__fixtures__/request'; -import { AuthenticationResult, DeauthenticationResult } from '../../../../../../../../plugins/security/server'; -import { initAuthenticateApi } from '../authenticate'; -import { KibanaRequest } from '../../../../../../../../../src/core/server'; - -describe('Authentication routes', () => { - let serverStub; - let hStub; - let loginStub; - let logoutStub; - - beforeEach(() => { - serverStub = serverFixture(); - hStub = { - authenticated: sinon.stub(), - continue: 'blah', - redirect: sinon.stub(), - response: sinon.stub() - }; - loginStub = sinon.stub(); - logoutStub = sinon.stub(); - - initAuthenticateApi({ - authc: { login: loginStub, logout: logoutStub }, - __legacyCompat: { config: { authc: { providers: ['basic'] } } }, - }, serverStub); - }); - - describe('login', () => { - let loginRoute; - let request; - - beforeEach(() => { - loginRoute = serverStub.route - .withArgs(sinon.match({ path: '/api/security/v1/login' })) - .firstCall - .args[0]; - - request = requestFixture({ - headers: {}, - payload: { username: 'user', password: 'password' } - }); - }); - - it('correctly defines route.', async () => { - expect(loginRoute.method).to.be('POST'); - expect(loginRoute.path).to.be('/api/security/v1/login'); - expect(loginRoute.handler).to.be.a(Function); - expect(loginRoute.config).to.eql({ - auth: false, - validate: { - payload: Joi.object({ - username: Joi.string().required(), - password: Joi.string().required() - }) - }, - response: { - emptyStatusCode: 204, - } - }); - }); - - it('returns 500 if authentication throws unhandled exception.', async () => { - const unhandledException = new Error('Something went wrong.'); - loginStub.throws(unhandledException); - - return loginRoute - .handler(request, hStub) - .catch((response) => { - expect(response.isBoom).to.be(true); - expect(response.output.payload).to.eql({ - statusCode: 500, - error: 'Internal Server Error', - message: 'An internal server error occurred' - }); - }); - }); - - it('returns 401 if authentication fails.', async () => { - const failureReason = new Error('Something went wrong.'); - loginStub.resolves(AuthenticationResult.failed(failureReason)); - - return loginRoute - .handler(request, hStub) - .catch((response) => { - expect(response.isBoom).to.be(true); - expect(response.message).to.be(failureReason.message); - expect(response.output.statusCode).to.be(401); - }); - }); - - it('returns 401 if authentication is not handled.', async () => { - loginStub.resolves(AuthenticationResult.notHandled()); - - return loginRoute - .handler(request, hStub) - .catch((response) => { - expect(response.isBoom).to.be(true); - expect(response.message).to.be('Unauthorized'); - expect(response.output.statusCode).to.be(401); - }); - }); - - describe('authentication succeeds', () => { - - it(`returns user data`, async () => { - loginStub.resolves(AuthenticationResult.succeeded({ username: 'user' })); - - await loginRoute.handler(request, hStub); - - sinon.assert.calledOnce(hStub.response); - sinon.assert.calledOnce(loginStub); - sinon.assert.calledWithExactly( - loginStub, - sinon.match.instanceOf(KibanaRequest), - { provider: 'basic', value: { username: 'user', password: 'password' } } - ); - }); - }); - - }); - - describe('logout', () => { - let logoutRoute; - - beforeEach(() => { - serverStub.config.returns({ - get: sinon.stub().withArgs('server.basePath').returns('/test-base-path') - }); - - logoutRoute = serverStub.route - .withArgs(sinon.match({ path: '/api/security/v1/logout' })) - .firstCall - .args[0]; - }); - - it('correctly defines route.', async () => { - expect(logoutRoute.method).to.be('GET'); - expect(logoutRoute.path).to.be('/api/security/v1/logout'); - expect(logoutRoute.handler).to.be.a(Function); - expect(logoutRoute.config).to.eql({ auth: false }); - }); - - it('returns 500 if deauthentication throws unhandled exception.', async () => { - const request = requestFixture(); - - const unhandledException = new Error('Something went wrong.'); - logoutStub.rejects(unhandledException); - - return logoutRoute - .handler(request, hStub) - .catch((response) => { - expect(response).to.be(Boom.boomify(unhandledException)); - sinon.assert.notCalled(hStub.redirect); - }); - }); - - it('returns 500 if authenticator fails to logout.', async () => { - const request = requestFixture(); - - const failureReason = Boom.forbidden(); - logoutStub.resolves(DeauthenticationResult.failed(failureReason)); - - return logoutRoute - .handler(request, hStub) - .catch((response) => { - expect(response).to.be(Boom.boomify(failureReason)); - sinon.assert.notCalled(hStub.redirect); - sinon.assert.calledOnce(logoutStub); - sinon.assert.calledWithExactly( - logoutStub, - sinon.match.instanceOf(KibanaRequest) - ); - }); - }); - - it('returns 400 for AJAX requests that can not handle redirect.', async () => { - const request = requestFixture({ headers: { 'kbn-xsrf': 'xsrf' } }); - - return logoutRoute - .handler(request, hStub) - .catch((response) => { - expect(response.isBoom).to.be(true); - expect(response.message).to.be('Client should be able to process redirect response.'); - expect(response.output.statusCode).to.be(400); - sinon.assert.notCalled(hStub.redirect); - }); - }); - - it('redirects user to the URL returned by authenticator.', async () => { - const request = requestFixture(); - - logoutStub.resolves(DeauthenticationResult.redirectTo('https://custom.logout')); - - await logoutRoute.handler(request, hStub); - - sinon.assert.calledOnce(hStub.redirect); - sinon.assert.calledWithExactly(hStub.redirect, 'https://custom.logout'); - }); - - it('redirects user to the base path if deauthentication succeeds.', async () => { - const request = requestFixture(); - - logoutStub.resolves(DeauthenticationResult.succeeded()); - - await logoutRoute.handler(request, hStub); - - sinon.assert.calledOnce(hStub.redirect); - sinon.assert.calledWithExactly(hStub.redirect, '/test-base-path/'); - }); - - it('redirects user to the base path if deauthentication is not handled.', async () => { - const request = requestFixture(); - - logoutStub.resolves(DeauthenticationResult.notHandled()); - - await logoutRoute.handler(request, hStub); - - sinon.assert.calledOnce(hStub.redirect); - sinon.assert.calledWithExactly(hStub.redirect, '/test-base-path/'); - }); - }); - - describe('me', () => { - let meRoute; - - beforeEach(() => { - meRoute = serverStub.route - .withArgs(sinon.match({ path: '/api/security/v1/me' })) - .firstCall - .args[0]; - }); - - it('correctly defines route.', async () => { - expect(meRoute.method).to.be('GET'); - expect(meRoute.path).to.be('/api/security/v1/me'); - expect(meRoute.handler).to.be.a(Function); - expect(meRoute.config).to.be(undefined); - }); - - it('returns user from the authenticated request property.', async () => { - const request = { auth: { credentials: { username: 'user' } } }; - const response = await meRoute.handler(request, hStub); - - expect(response).to.eql({ username: 'user' }); - }); - }); -}); diff --git a/x-pack/legacy/plugins/security/server/routes/api/v1/__tests__/users.js b/x-pack/legacy/plugins/security/server/routes/api/v1/__tests__/users.js deleted file mode 100644 index 4077ab52e86de..0000000000000 --- a/x-pack/legacy/plugins/security/server/routes/api/v1/__tests__/users.js +++ /dev/null @@ -1,214 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from '@kbn/expect'; -import Joi from 'joi'; -import sinon from 'sinon'; - -import { serverFixture } from '../../../../lib/__tests__/__fixtures__/server'; -import { requestFixture } from '../../../../lib/__tests__/__fixtures__/request'; -import { AuthenticationResult } from '../../../../../../../../plugins/security/server'; -import { initUsersApi } from '../users'; -import * as ClientShield from '../../../../../../../server/lib/get_client_shield'; -import { KibanaRequest } from '../../../../../../../../../src/core/server'; - -describe('User routes', () => { - const sandbox = sinon.createSandbox(); - - let clusterStub; - let serverStub; - let loginStub; - - beforeEach(() => { - serverStub = serverFixture(); - loginStub = sinon.stub(); - - // Cluster is returned by `getClient` function that is wrapped into `once` making cluster - // a static singleton, so we should use sandbox to set/reset its behavior between tests. - clusterStub = sinon.stub({ callWithRequest() {} }); - sandbox.stub(ClientShield, 'getClient').returns(clusterStub); - - initUsersApi({ authc: { login: loginStub }, __legacyCompat: { config: { authc: { providers: ['basic'] } } } }, serverStub); - }); - - afterEach(() => sandbox.restore()); - - describe('change password', () => { - let changePasswordRoute; - let request; - - beforeEach(() => { - changePasswordRoute = serverStub.route - .withArgs(sinon.match({ path: '/api/security/v1/users/{username}/password' })) - .firstCall - .args[0]; - - request = requestFixture({ - headers: {}, - auth: { credentials: { username: 'user' } }, - params: { username: 'target-user' }, - payload: { password: 'old-password', newPassword: 'new-password' } - }); - }); - - it('correctly defines route.', async () => { - expect(changePasswordRoute.method).to.be('POST'); - expect(changePasswordRoute.path).to.be('/api/security/v1/users/{username}/password'); - expect(changePasswordRoute.handler).to.be.a(Function); - - expect(changePasswordRoute.config).to.not.have.property('auth'); - expect(changePasswordRoute.config).to.have.property('pre'); - expect(changePasswordRoute.config.pre).to.have.length(1); - expect(changePasswordRoute.config.validate).to.eql({ - payload: Joi.object({ - password: Joi.string(), - newPassword: Joi.string().required() - }) - }); - }); - - describe('own password', () => { - beforeEach(() => { - request.params.username = request.auth.credentials.username; - loginStub = loginStub - .withArgs( - sinon.match.instanceOf(KibanaRequest), - { provider: 'basic', value: { username: 'user', password: 'old-password' }, stateless: true } - ) - .resolves(AuthenticationResult.succeeded({})); - }); - - it('returns 403 if old password is wrong.', async () => { - loginStub.resolves(AuthenticationResult.failed(new Error('Something went wrong.'))); - - const response = await changePasswordRoute.handler(request); - - sinon.assert.notCalled(clusterStub.callWithRequest); - expect(response.isBoom).to.be(true); - expect(response.output.payload).to.eql({ - statusCode: 403, - error: 'Forbidden', - message: 'Something went wrong.' - }); - }); - - it(`returns 401 if user can't authenticate with new password.`, async () => { - loginStub - .withArgs( - sinon.match.instanceOf(KibanaRequest), - { provider: 'basic', value: { username: 'user', password: 'new-password' } } - ) - .resolves(AuthenticationResult.failed(new Error('Something went wrong.'))); - - const response = await changePasswordRoute.handler(request); - - sinon.assert.calledOnce(clusterStub.callWithRequest); - sinon.assert.calledWithExactly( - clusterStub.callWithRequest, - sinon.match.same(request), - 'shield.changePassword', - { username: 'user', body: { password: 'new-password' } } - ); - - expect(response.isBoom).to.be(true); - expect(response.output.payload).to.eql({ - statusCode: 401, - error: 'Unauthorized', - message: 'Something went wrong.' - }); - }); - - it('returns 500 if password update request fails.', async () => { - clusterStub.callWithRequest - .withArgs( - sinon.match.same(request), - 'shield.changePassword', - { username: 'user', body: { password: 'new-password' } } - ) - .rejects(new Error('Request failed.')); - - const response = await changePasswordRoute.handler(request); - - expect(response.isBoom).to.be(true); - expect(response.output.payload).to.eql({ - statusCode: 500, - error: 'Internal Server Error', - message: 'An internal server error occurred' - }); - }); - - it('successfully changes own password if provided old password is correct.', async () => { - loginStub - .withArgs( - sinon.match.instanceOf(KibanaRequest), - { provider: 'basic', value: { username: 'user', password: 'new-password' } } - ) - .resolves(AuthenticationResult.succeeded({})); - - const hResponseStub = { code: sinon.stub() }; - const hStub = { response: sinon.stub().returns(hResponseStub) }; - - await changePasswordRoute.handler(request, hStub); - - sinon.assert.calledOnce(clusterStub.callWithRequest); - sinon.assert.calledWithExactly( - clusterStub.callWithRequest, - sinon.match.same(request), - 'shield.changePassword', - { username: 'user', body: { password: 'new-password' } } - ); - - sinon.assert.calledWithExactly(hStub.response); - sinon.assert.calledWithExactly(hResponseStub.code, 204); - }); - }); - - describe('other user password', () => { - it('returns 500 if password update request fails.', async () => { - clusterStub.callWithRequest - .withArgs( - sinon.match.same(request), - 'shield.changePassword', - { username: 'target-user', body: { password: 'new-password' } } - ) - .returns(Promise.reject(new Error('Request failed.'))); - - const response = await changePasswordRoute.handler(request); - - sinon.assert.notCalled(serverStub.plugins.security.getUser); - sinon.assert.notCalled(loginStub); - - expect(response.isBoom).to.be(true); - expect(response.output.payload).to.eql({ - statusCode: 500, - error: 'Internal Server Error', - message: 'An internal server error occurred' - }); - }); - - it('successfully changes user password.', async () => { - const hResponseStub = { code: sinon.stub() }; - const hStub = { response: sinon.stub().returns(hResponseStub) }; - - await changePasswordRoute.handler(request, hStub); - - sinon.assert.notCalled(serverStub.plugins.security.getUser); - sinon.assert.notCalled(loginStub); - - sinon.assert.calledOnce(clusterStub.callWithRequest); - sinon.assert.calledWithExactly( - clusterStub.callWithRequest, - sinon.match.same(request), - 'shield.changePassword', - { username: 'target-user', body: { password: 'new-password' } } - ); - - sinon.assert.calledWithExactly(hStub.response); - sinon.assert.calledWithExactly(hResponseStub.code, 204); - }); - }); - }); -}); diff --git a/x-pack/legacy/plugins/security/server/routes/api/v1/api_keys/get.js b/x-pack/legacy/plugins/security/server/routes/api/v1/api_keys/get.js deleted file mode 100644 index a236badcd0d6b..0000000000000 --- a/x-pack/legacy/plugins/security/server/routes/api/v1/api_keys/get.js +++ /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; - * you may not use this file except in compliance with the Elastic License. - */ - -import Joi from 'joi'; -import { wrapError } from '../../../../../../../../plugins/security/server'; -import { INTERNAL_API_BASE_PATH } from '../../../../../common/constants'; - -export function initGetApiKeysApi(server, callWithRequest, routePreCheckLicenseFn) { - server.route({ - method: 'GET', - path: `${INTERNAL_API_BASE_PATH}/api_key`, - async handler(request) { - try { - const { isAdmin } = request.query; - - const result = await callWithRequest( - request, - 'shield.getAPIKeys', - { - owner: !isAdmin - } - ); - - const validKeys = result.api_keys.filter(({ invalidated }) => !invalidated); - - return { - apiKeys: validKeys, - }; - } catch (error) { - return wrapError(error); - } - }, - config: { - pre: [routePreCheckLicenseFn], - validate: { - query: Joi.object().keys({ - isAdmin: Joi.bool().required(), - }).required(), - }, - } - }); -} diff --git a/x-pack/legacy/plugins/security/server/routes/api/v1/api_keys/get.test.js b/x-pack/legacy/plugins/security/server/routes/api/v1/api_keys/get.test.js deleted file mode 100644 index 400e5b705aeb2..0000000000000 --- a/x-pack/legacy/plugins/security/server/routes/api/v1/api_keys/get.test.js +++ /dev/null @@ -1,166 +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; - * you may not use this file except in compliance with the Elastic License. - */ -import Hapi from 'hapi'; -import Boom from 'boom'; - -import { initGetApiKeysApi } from './get'; -import { INTERNAL_API_BASE_PATH } from '../../../../../common/constants'; - -const createMockServer = () => new Hapi.Server({ debug: false, port: 8080 }); - -describe('GET API keys', () => { - const getApiKeysTest = ( - description, - { - preCheckLicenseImpl = () => null, - callWithRequestImpl, - asserts, - isAdmin = true, - } - ) => { - test(description, async () => { - const mockServer = createMockServer(); - const pre = jest.fn().mockImplementation(preCheckLicenseImpl); - const mockCallWithRequest = jest.fn(); - - if (callWithRequestImpl) { - mockCallWithRequest.mockImplementation(callWithRequestImpl); - } - - initGetApiKeysApi(mockServer, mockCallWithRequest, pre); - - const headers = { - authorization: 'foo', - }; - - const request = { - method: 'GET', - url: `${INTERNAL_API_BASE_PATH}/api_key?isAdmin=${isAdmin}`, - headers, - }; - - const { result, statusCode } = await mockServer.inject(request); - - expect(pre).toHaveBeenCalled(); - - if (callWithRequestImpl) { - expect(mockCallWithRequest).toHaveBeenCalledWith( - expect.objectContaining({ - headers: expect.objectContaining({ - authorization: headers.authorization, - }), - }), - 'shield.getAPIKeys', - { - owner: !isAdmin, - }, - ); - } else { - expect(mockCallWithRequest).not.toHaveBeenCalled(); - } - - expect(statusCode).toBe(asserts.statusCode); - expect(result).toEqual(asserts.result); - }); - }; - - describe('failure', () => { - getApiKeysTest('returns result of routePreCheckLicense', { - preCheckLicenseImpl: () => Boom.forbidden('test forbidden message'), - asserts: { - statusCode: 403, - result: { - error: 'Forbidden', - statusCode: 403, - message: 'test forbidden message', - }, - }, - }); - - getApiKeysTest('returns error from callWithRequest', { - callWithRequestImpl: async () => { - throw Boom.notAcceptable('test not acceptable message'); - }, - asserts: { - statusCode: 406, - result: { - error: 'Not Acceptable', - statusCode: 406, - message: 'test not acceptable message', - }, - }, - }); - }); - - describe('success', () => { - getApiKeysTest('returns API keys', { - callWithRequestImpl: async () => ({ - api_keys: - [{ - id: 'YCLV7m0BJ3xI4hhWB648', - name: 'test-api-key', - creation: 1571670001452, - expiration: 1571756401452, - invalidated: false, - username: 'elastic', - realm: 'reserved' - }] - }), - asserts: { - statusCode: 200, - result: { - apiKeys: - [{ - id: 'YCLV7m0BJ3xI4hhWB648', - name: 'test-api-key', - creation: 1571670001452, - expiration: 1571756401452, - invalidated: false, - username: 'elastic', - realm: 'reserved' - }] - }, - }, - }); - getApiKeysTest('returns only valid API keys', { - callWithRequestImpl: async () => ({ - api_keys: - [{ - id: 'YCLV7m0BJ3xI4hhWB648', - name: 'test-api-key1', - creation: 1571670001452, - expiration: 1571756401452, - invalidated: true, - username: 'elastic', - realm: 'reserved' - }, { - id: 'YCLV7m0BJ3xI4hhWB648', - name: 'test-api-key2', - creation: 1571670001452, - expiration: 1571756401452, - invalidated: false, - username: 'elastic', - realm: 'reserved' - }], - }), - asserts: { - statusCode: 200, - result: { - apiKeys: - [{ - id: 'YCLV7m0BJ3xI4hhWB648', - name: 'test-api-key2', - creation: 1571670001452, - expiration: 1571756401452, - invalidated: false, - username: 'elastic', - realm: 'reserved' - }] - }, - }, - }); - }); -}); diff --git a/x-pack/legacy/plugins/security/server/routes/api/v1/api_keys/index.js b/x-pack/legacy/plugins/security/server/routes/api/v1/api_keys/index.js deleted file mode 100644 index fc55bdcc38661..0000000000000 --- a/x-pack/legacy/plugins/security/server/routes/api/v1/api_keys/index.js +++ /dev/null @@ -1,20 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { getClient } from '../../../../../../../server/lib/get_client_shield'; -import { routePreCheckLicense } from '../../../../lib/route_pre_check_license'; -import { initCheckPrivilegesApi } from './privileges'; -import { initGetApiKeysApi } from './get'; -import { initInvalidateApiKeysApi } from './invalidate'; - -export function initApiKeysApi(server) { - const callWithRequest = getClient(server).callWithRequest; - const routePreCheckLicenseFn = routePreCheckLicense(server); - - initCheckPrivilegesApi(server, callWithRequest, routePreCheckLicenseFn); - initGetApiKeysApi(server, callWithRequest, routePreCheckLicenseFn); - initInvalidateApiKeysApi(server, callWithRequest, routePreCheckLicenseFn); -} diff --git a/x-pack/legacy/plugins/security/server/routes/api/v1/api_keys/invalidate.js b/x-pack/legacy/plugins/security/server/routes/api/v1/api_keys/invalidate.js deleted file mode 100644 index 293142c60be67..0000000000000 --- a/x-pack/legacy/plugins/security/server/routes/api/v1/api_keys/invalidate.js +++ /dev/null @@ -1,70 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import Joi from 'joi'; -import { wrapError } from '../../../../../../../../plugins/security/server'; -import { INTERNAL_API_BASE_PATH } from '../../../../../common/constants'; - -export function initInvalidateApiKeysApi(server, callWithRequest, routePreCheckLicenseFn) { - server.route({ - method: 'POST', - path: `${INTERNAL_API_BASE_PATH}/api_key/invalidate`, - async handler(request) { - try { - const { apiKeys, isAdmin } = request.payload; - const itemsInvalidated = []; - const errors = []; - - // Send the request to invalidate the API key and return an error if it could not be deleted. - const sendRequestToInvalidateApiKey = async (id) => { - try { - const body = { id }; - - if (!isAdmin) { - body.owner = true; - } - - await callWithRequest(request, 'shield.invalidateAPIKey', { body }); - return null; - } catch (error) { - return wrapError(error); - } - }; - - const invalidateApiKey = async ({ id, name }) => { - const error = await sendRequestToInvalidateApiKey(id); - if (error) { - errors.push({ id, name, error }); - } else { - itemsInvalidated.push({ id, name }); - } - }; - - // Invalidate all API keys in parallel. - await Promise.all(apiKeys.map((key) => invalidateApiKey(key))); - - return { - itemsInvalidated, - errors, - }; - } catch (error) { - return wrapError(error); - } - }, - config: { - pre: [routePreCheckLicenseFn], - validate: { - payload: Joi.object({ - apiKeys: Joi.array().items(Joi.object({ - id: Joi.string().required(), - name: Joi.string().required(), - })).required(), - isAdmin: Joi.bool().required(), - }) - }, - } - }); -} diff --git a/x-pack/legacy/plugins/security/server/routes/api/v1/api_keys/invalidate.test.js b/x-pack/legacy/plugins/security/server/routes/api/v1/api_keys/invalidate.test.js deleted file mode 100644 index 3ed7ca94eb782..0000000000000 --- a/x-pack/legacy/plugins/security/server/routes/api/v1/api_keys/invalidate.test.js +++ /dev/null @@ -1,200 +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; - * you may not use this file except in compliance with the Elastic License. - */ -import Hapi from 'hapi'; -import Boom from 'boom'; - -import { initInvalidateApiKeysApi } from './invalidate'; -import { INTERNAL_API_BASE_PATH } from '../../../../../common/constants'; - -const createMockServer = () => new Hapi.Server({ debug: false, port: 8080 }); - -describe('POST invalidate', () => { - const postInvalidateTest = ( - description, - { - preCheckLicenseImpl = () => null, - callWithRequestImpls = [], - asserts, - payload, - } - ) => { - test(description, async () => { - const mockServer = createMockServer(); - const pre = jest.fn().mockImplementation(preCheckLicenseImpl); - const mockCallWithRequest = jest.fn(); - - for (const impl of callWithRequestImpls) { - mockCallWithRequest.mockImplementationOnce(impl); - } - - initInvalidateApiKeysApi(mockServer, mockCallWithRequest, pre); - - const headers = { - authorization: 'foo', - }; - - const request = { - method: 'POST', - url: `${INTERNAL_API_BASE_PATH}/api_key/invalidate`, - headers, - payload, - }; - - const { result, statusCode } = await mockServer.inject(request); - - expect(pre).toHaveBeenCalled(); - - if (asserts.callWithRequests) { - for (const args of asserts.callWithRequests) { - expect(mockCallWithRequest).toHaveBeenCalledWith( - expect.objectContaining({ - headers: expect.objectContaining({ - authorization: headers.authorization, - }), - }), - ...args - ); - } - } else { - expect(mockCallWithRequest).not.toHaveBeenCalled(); - } - - expect(statusCode).toBe(asserts.statusCode); - expect(result).toEqual(asserts.result); - }); - }; - - describe('failure', () => { - postInvalidateTest('returns result of routePreCheckLicense', { - preCheckLicenseImpl: () => Boom.forbidden('test forbidden message'), - payload: { - apiKeys: [{ id: 'si8If24B1bKsmSLTAhJV', name: 'my-api-key' }], - isAdmin: true - }, - asserts: { - statusCode: 403, - result: { - error: 'Forbidden', - statusCode: 403, - message: 'test forbidden message', - }, - }, - }); - - postInvalidateTest('returns errors array from callWithRequest', { - callWithRequestImpls: [async () => { - throw Boom.notAcceptable('test not acceptable message'); - }], - payload: { - apiKeys: [{ id: 'si8If24B1bKsmSLTAhJV', name: 'my-api-key', }], - isAdmin: true - }, - asserts: { - callWithRequests: [ - ['shield.invalidateAPIKey', { - body: { - id: 'si8If24B1bKsmSLTAhJV', - }, - }], - ], - statusCode: 200, - result: { - itemsInvalidated: [], - errors: [{ - id: 'si8If24B1bKsmSLTAhJV', - name: 'my-api-key', - error: Boom.notAcceptable('test not acceptable message'), - }] - }, - }, - }); - }); - - describe('success', () => { - postInvalidateTest('invalidates API keys', { - callWithRequestImpls: [async () => null], - payload: { - apiKeys: [{ id: 'si8If24B1bKsmSLTAhJV', name: 'my-api-key', }], - isAdmin: true - }, - asserts: { - callWithRequests: [ - ['shield.invalidateAPIKey', { - body: { - id: 'si8If24B1bKsmSLTAhJV', - }, - }], - ], - statusCode: 200, - result: { - itemsInvalidated: [{ id: 'si8If24B1bKsmSLTAhJV', name: 'my-api-key', }], - errors: [], - }, - }, - }); - - postInvalidateTest('adds "owner" to body if isAdmin=false', { - callWithRequestImpls: [async () => null], - payload: { - apiKeys: [{ id: 'si8If24B1bKsmSLTAhJV', name: 'my-api-key', }], - isAdmin: false - }, - asserts: { - callWithRequests: [ - ['shield.invalidateAPIKey', { - body: { - id: 'si8If24B1bKsmSLTAhJV', - owner: true, - }, - }], - ], - statusCode: 200, - result: { - itemsInvalidated: [{ id: 'si8If24B1bKsmSLTAhJV', name: 'my-api-key' }], - errors: [], - }, - }, - }); - - postInvalidateTest('returns only successful invalidation requests', { - callWithRequestImpls: [ - async () => null, - async () => { - throw Boom.notAcceptable('test not acceptable message'); - }], - payload: { - apiKeys: [ - { id: 'si8If24B1bKsmSLTAhJV', name: 'my-api-key1' }, - { id: 'ab8If24B1bKsmSLTAhNC', name: 'my-api-key2' } - ], - isAdmin: true - }, - asserts: { - callWithRequests: [ - ['shield.invalidateAPIKey', { - body: { - id: 'si8If24B1bKsmSLTAhJV', - }, - }], - ['shield.invalidateAPIKey', { - body: { - id: 'ab8If24B1bKsmSLTAhNC', - }, - }], - ], - statusCode: 200, - result: { - itemsInvalidated: [{ id: 'si8If24B1bKsmSLTAhJV', name: 'my-api-key1' }], - errors: [{ - id: 'ab8If24B1bKsmSLTAhNC', - name: 'my-api-key2', - error: Boom.notAcceptable('test not acceptable message'), - }] - }, - }, - }); - }); -}); diff --git a/x-pack/legacy/plugins/security/server/routes/api/v1/api_keys/privileges.js b/x-pack/legacy/plugins/security/server/routes/api/v1/api_keys/privileges.js deleted file mode 100644 index 3aa30c9a3b9bb..0000000000000 --- a/x-pack/legacy/plugins/security/server/routes/api/v1/api_keys/privileges.js +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { wrapError } from '../../../../../../../../plugins/security/server'; -import { INTERNAL_API_BASE_PATH } from '../../../../../common/constants'; - -export function initCheckPrivilegesApi(server, callWithRequest, routePreCheckLicenseFn) { - server.route({ - method: 'GET', - path: `${INTERNAL_API_BASE_PATH}/api_key/privileges`, - async handler(request) { - try { - const result = await Promise.all([ - callWithRequest( - request, - 'shield.hasPrivileges', - { - body: { - cluster: [ - 'manage_security', - 'manage_api_key', - ], - }, - } - ), - new Promise(async (resolve, reject) => { - try { - const result = await callWithRequest( - request, - 'shield.getAPIKeys', - { - owner: true - } - ); - // If the API returns a truthy result that means it's enabled. - resolve({ areApiKeysEnabled: !!result }); - } catch (e) { - // This is a brittle dependency upon message. Tracked by https://github.com/elastic/elasticsearch/issues/47759. - if (e.message.includes('api keys are not enabled')) { - return resolve({ areApiKeysEnabled: false }); - } - - // It's a real error, so rethrow it. - reject(e); - } - }), - ]); - - const [{ - cluster: { - manage_security: manageSecurity, - manage_api_key: manageApiKey, - } - }, { - areApiKeysEnabled, - }] = result; - - const isAdmin = manageSecurity || manageApiKey; - - return { - areApiKeysEnabled, - isAdmin, - }; - } catch (error) { - return wrapError(error); - } - }, - config: { - pre: [routePreCheckLicenseFn] - } - }); -} diff --git a/x-pack/legacy/plugins/security/server/routes/api/v1/api_keys/privileges.test.js b/x-pack/legacy/plugins/security/server/routes/api/v1/api_keys/privileges.test.js deleted file mode 100644 index 2a6f935e00595..0000000000000 --- a/x-pack/legacy/plugins/security/server/routes/api/v1/api_keys/privileges.test.js +++ /dev/null @@ -1,254 +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; - * you may not use this file except in compliance with the Elastic License. - */ -import Hapi from 'hapi'; -import Boom from 'boom'; - -import { initCheckPrivilegesApi } from './privileges'; -import { INTERNAL_API_BASE_PATH } from '../../../../../common/constants'; - -const createMockServer = () => new Hapi.Server({ debug: false, port: 8080 }); - -describe('GET privileges', () => { - const getPrivilegesTest = ( - description, - { - preCheckLicenseImpl = () => null, - callWithRequestImpls = [], - asserts, - } - ) => { - test(description, async () => { - const mockServer = createMockServer(); - const pre = jest.fn().mockImplementation(preCheckLicenseImpl); - const mockCallWithRequest = jest.fn(); - - for (const impl of callWithRequestImpls) { - mockCallWithRequest.mockImplementationOnce(impl); - } - - initCheckPrivilegesApi(mockServer, mockCallWithRequest, pre); - - const headers = { - authorization: 'foo', - }; - - const request = { - method: 'GET', - url: `${INTERNAL_API_BASE_PATH}/api_key/privileges`, - headers, - }; - - const { result, statusCode } = await mockServer.inject(request); - - expect(pre).toHaveBeenCalled(); - - if (asserts.callWithRequests) { - for (const args of asserts.callWithRequests) { - expect(mockCallWithRequest).toHaveBeenCalledWith( - expect.objectContaining({ - headers: expect.objectContaining({ - authorization: headers.authorization, - }), - }), - ...args - ); - } - } else { - expect(mockCallWithRequest).not.toHaveBeenCalled(); - } - - expect(statusCode).toBe(asserts.statusCode); - expect(result).toEqual(asserts.result); - }); - }; - - describe('failure', () => { - getPrivilegesTest('returns result of routePreCheckLicense', { - preCheckLicenseImpl: () => Boom.forbidden('test forbidden message'), - asserts: { - statusCode: 403, - result: { - error: 'Forbidden', - statusCode: 403, - message: 'test forbidden message', - }, - }, - }); - - getPrivilegesTest('returns error from first callWithRequest', { - callWithRequestImpls: [async () => { - throw Boom.notAcceptable('test not acceptable message'); - }, async () => { }], - asserts: { - callWithRequests: [ - ['shield.hasPrivileges', { - body: { - cluster: [ - 'manage_security', - 'manage_api_key', - ], - }, - }], - ['shield.getAPIKeys', { owner: true }], - ], - statusCode: 406, - result: { - error: 'Not Acceptable', - statusCode: 406, - message: 'test not acceptable message', - }, - }, - }); - - getPrivilegesTest('returns error from second callWithRequest', { - callWithRequestImpls: [async () => { }, async () => { - throw Boom.notAcceptable('test not acceptable message'); - }], - asserts: { - callWithRequests: [ - ['shield.hasPrivileges', { - body: { - cluster: [ - 'manage_security', - 'manage_api_key', - ], - }, - }], - ['shield.getAPIKeys', { owner: true }], - ], - statusCode: 406, - result: { - error: 'Not Acceptable', - statusCode: 406, - message: 'test not acceptable message', - }, - }, - }); - }); - - describe('success', () => { - getPrivilegesTest('returns areApiKeysEnabled and isAdmin', { - callWithRequestImpls: [ - async () => ({ - username: 'elastic', - has_all_requested: true, - cluster: { manage_api_key: true, manage_security: true }, - index: {}, - application: {} - }), - async () => ( - { - api_keys: - [{ - id: 'si8If24B1bKsmSLTAhJV', - name: 'my-api-key', - creation: 1574089261632, - expiration: 1574175661632, - invalidated: false, - username: 'elastic', - realm: 'reserved' - }] - } - ), - ], - asserts: { - callWithRequests: [ - ['shield.getAPIKeys', { owner: true }], - ['shield.hasPrivileges', { - body: { - cluster: [ - 'manage_security', - 'manage_api_key', - ], - }, - }], - ], - statusCode: 200, - result: { - areApiKeysEnabled: true, - isAdmin: true, - }, - }, - }); - - getPrivilegesTest('returns areApiKeysEnabled=false when getAPIKeys error message includes "api keys are not enabled"', { - callWithRequestImpls: [ - async () => ({ - username: 'elastic', - has_all_requested: true, - cluster: { manage_api_key: true, manage_security: true }, - index: {}, - application: {} - }), - async () => { - throw Boom.unauthorized('api keys are not enabled'); - }, - ], - asserts: { - callWithRequests: [ - ['shield.getAPIKeys', { owner: true }], - ['shield.hasPrivileges', { - body: { - cluster: [ - 'manage_security', - 'manage_api_key', - ], - }, - }], - ], - statusCode: 200, - result: { - areApiKeysEnabled: false, - isAdmin: true, - }, - }, - }); - - getPrivilegesTest('returns isAdmin=false when user has insufficient privileges', { - callWithRequestImpls: [ - async () => ({ - username: 'elastic', - has_all_requested: true, - cluster: { manage_api_key: false, manage_security: false }, - index: {}, - application: {} - }), - async () => ( - { - api_keys: - [{ - id: 'si8If24B1bKsmSLTAhJV', - name: 'my-api-key', - creation: 1574089261632, - expiration: 1574175661632, - invalidated: false, - username: 'elastic', - realm: 'reserved' - }] - } - ), - ], - asserts: { - callWithRequests: [ - ['shield.getAPIKeys', { owner: true }], - ['shield.hasPrivileges', { - body: { - cluster: [ - 'manage_security', - 'manage_api_key', - ], - }, - }], - ], - statusCode: 200, - result: { - areApiKeysEnabled: true, - isAdmin: false, - }, - }, - }); - }); -}); diff --git a/x-pack/legacy/plugins/security/server/routes/api/v1/authenticate.js b/x-pack/legacy/plugins/security/server/routes/api/v1/authenticate.js deleted file mode 100644 index f37c9a2fd917f..0000000000000 --- a/x-pack/legacy/plugins/security/server/routes/api/v1/authenticate.js +++ /dev/null @@ -1,227 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import Boom from 'boom'; -import Joi from 'joi'; -import { schema } from '@kbn/config-schema'; -import { canRedirectRequest, wrapError, OIDCAuthenticationFlow } from '../../../../../../../plugins/security/server'; -import { KibanaRequest } from '../../../../../../../../src/core/server'; -import { createCSPRuleString } from '../../../../../../../../src/legacy/server/csp'; - -export function initAuthenticateApi({ authc: { login, logout }, __legacyCompat: { config } }, server) { - function prepareCustomResourceResponse(response, contentType) { - return response - .header('cache-control', 'private, no-cache, no-store') - .header('content-security-policy', createCSPRuleString(server.config().get('csp.rules'))) - .type(contentType); - } - - server.route({ - method: 'POST', - path: '/api/security/v1/login', - config: { - auth: false, - validate: { - payload: Joi.object({ - username: Joi.string().required(), - password: Joi.string().required() - }) - }, - response: { - emptyStatusCode: 204, - } - }, - async handler(request, h) { - const { username, password } = request.payload; - - try { - // We should prefer `token` over `basic` if possible. - const providerToLoginWith = config.authc.providers.includes('token') - ? 'token' - : 'basic'; - const authenticationResult = await login(KibanaRequest.from(request), { - provider: providerToLoginWith, - value: { username, password } - }); - - if (!authenticationResult.succeeded()) { - throw Boom.unauthorized(authenticationResult.error); - } - - return h.response(); - } catch(err) { - throw wrapError(err); - } - } - }); - - /** - * The route should be configured as a redirect URI in OP when OpenID Connect implicit flow - * is used, so that we can extract authentication response from URL fragment and send it to - * the `/api/security/v1/oidc` route. - */ - server.route({ - method: 'GET', - path: '/api/security/v1/oidc/implicit', - config: { auth: false }, - async handler(request, h) { - return prepareCustomResourceResponse( - h.response(` - - Kibana OpenID Connect Login - - `), - 'text/html' - ); - } - }); - - /** - * The route that accompanies `/api/security/v1/oidc/implicit` and renders a JavaScript snippet - * that extracts fragment part from the URL and send it to the `/api/security/v1/oidc` route. - * We need this separate endpoint because of default CSP policy that forbids inline scripts. - */ - server.route({ - method: 'GET', - path: '/api/security/v1/oidc/implicit.js', - config: { auth: false }, - async handler(request, h) { - return prepareCustomResourceResponse( - h.response(` - window.location.replace( - '${server.config().get('server.basePath')}/api/security/v1/oidc?authenticationResponseURI=' + - encodeURIComponent(window.location.href) - ); - `), - 'text/javascript' - ); - } - }); - - server.route({ - // POST is only allowed for Third Party initiated authentication - // Consider splitting this route into two (GET and POST) when it's migrated to New Platform. - method: ['GET', 'POST'], - path: '/api/security/v1/oidc', - config: { - auth: false, - validate: { - query: Joi.object().keys({ - iss: Joi.string().uri({ scheme: 'https' }), - login_hint: Joi.string(), - target_link_uri: Joi.string().uri(), - code: Joi.string(), - error: Joi.string(), - error_description: Joi.string(), - error_uri: Joi.string().uri(), - state: Joi.string(), - authenticationResponseURI: Joi.string(), - }).unknown(), - } - }, - async handler(request, h) { - try { - const query = request.query || {}; - const payload = request.payload || {}; - - // An HTTP GET request with a query parameter named `authenticationResponseURI` that includes URL fragment OpenID - // Connect Provider sent during implicit authentication flow to the Kibana own proxy page that extracted that URL - // fragment and put it into `authenticationResponseURI` query string parameter for this endpoint. See more details - // at https://openid.net/specs/openid-connect-core-1_0.html#ImplicitFlowAuth - let loginAttempt; - if (query.authenticationResponseURI) { - loginAttempt = { - flow: OIDCAuthenticationFlow.Implicit, - authenticationResponseURI: query.authenticationResponseURI, - }; - } else if (query.code || query.error) { - // An HTTP GET request with a query parameter named `code` (or `error`) as the response to a successful (or - // failed) authentication from an OpenID Connect Provider during authorization code authentication flow. - // See more details at https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth. - loginAttempt = { - flow: OIDCAuthenticationFlow.AuthorizationCode, - // We pass the path only as we can't be sure of the full URL and Elasticsearch doesn't need it anyway. - authenticationResponseURI: request.url.path, - }; - } else if (query.iss || payload.iss) { - // An HTTP GET request with a query parameter named `iss` or an HTTP POST request with the same parameter in the - // payload as part of a 3rd party initiated authentication. See more details at - // https://openid.net/specs/openid-connect-core-1_0.html#ThirdPartyInitiatedLogin - loginAttempt = { - flow: OIDCAuthenticationFlow.InitiatedBy3rdParty, - iss: query.iss || payload.iss, - loginHint: query.login_hint || payload.login_hint, - }; - } - - if (!loginAttempt) { - throw Boom.badRequest('Unrecognized login attempt.'); - } - - // We handle the fact that the user might get redirected to Kibana while already having an session - // Return an error notifying the user they are already logged in. - const authenticationResult = await login(KibanaRequest.from(request), { - provider: 'oidc', - value: loginAttempt - }); - if (authenticationResult.succeeded()) { - return Boom.forbidden( - 'Sorry, you already have an active Kibana session. ' + - 'If you want to start a new one, please logout from the existing session first.' - ); - } - - if (authenticationResult.redirected()) { - return h.redirect(authenticationResult.redirectURL); - } - - throw Boom.unauthorized(authenticationResult.error); - } catch (err) { - throw wrapError(err); - } - } - }); - - server.route({ - method: 'GET', - path: '/api/security/v1/logout', - config: { - auth: false - }, - async handler(request, h) { - if (!canRedirectRequest(KibanaRequest.from(request))) { - throw Boom.badRequest('Client should be able to process redirect response.'); - } - - try { - const deauthenticationResult = await logout( - // Allow unknown query parameters as this endpoint can be hit by the 3rd-party with any - // set of query string parameters (e.g. SAML/OIDC logout request parameters). - KibanaRequest.from(request, { - query: schema.object({}, { allowUnknowns: true }), - }) - ); - if (deauthenticationResult.failed()) { - throw wrapError(deauthenticationResult.error); - } - - return h.redirect( - deauthenticationResult.redirectURL || `${server.config().get('server.basePath')}/` - ); - } catch (err) { - throw wrapError(err); - } - } - }); - - server.route({ - method: 'GET', - path: '/api/security/v1/me', - handler(request) { - return request.auth.credentials; - } - }); -} diff --git a/x-pack/legacy/plugins/security/server/routes/api/v1/indices.js b/x-pack/legacy/plugins/security/server/routes/api/v1/indices.js deleted file mode 100644 index 7265b83783fdd..0000000000000 --- a/x-pack/legacy/plugins/security/server/routes/api/v1/indices.js +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import _ from 'lodash'; -import { getClient } from '../../../../../../server/lib/get_client_shield'; -import { wrapError } from '../../../../../../../plugins/security/server'; - -export function initIndicesApi(server) { - const callWithRequest = getClient(server).callWithRequest; - - server.route({ - method: 'GET', - path: '/api/security/v1/fields/{query}', - handler(request) { - return callWithRequest(request, 'indices.getFieldMapping', { - index: request.params.query, - fields: '*', - allowNoIndices: false, - includeDefaults: true - }) - .then((mappings) => - _(mappings) - .map('mappings') - .flatten() - .map(_.keys) - .flatten() - .uniq() - .value() - ) - .catch(wrapError); - } - }); -} diff --git a/x-pack/legacy/plugins/security/server/routes/api/v1/users.js b/x-pack/legacy/plugins/security/server/routes/api/v1/users.js deleted file mode 100644 index d6dc39da657b1..0000000000000 --- a/x-pack/legacy/plugins/security/server/routes/api/v1/users.js +++ /dev/null @@ -1,142 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import _ from 'lodash'; -import Boom from 'boom'; -import Joi from 'joi'; -import { getClient } from '../../../../../../server/lib/get_client_shield'; -import { userSchema } from '../../../lib/user_schema'; -import { routePreCheckLicense } from '../../../lib/route_pre_check_license'; -import { wrapError } from '../../../../../../../plugins/security/server'; -import { KibanaRequest } from '../../../../../../../../src/core/server'; - -export function initUsersApi({ authc: { login }, __legacyCompat: { config } }, server) { - const callWithRequest = getClient(server).callWithRequest; - const routePreCheckLicenseFn = routePreCheckLicense(server); - - server.route({ - method: 'GET', - path: '/api/security/v1/users', - handler(request) { - return callWithRequest(request, 'shield.getUser').then( - _.values, - wrapError - ); - }, - config: { - pre: [routePreCheckLicenseFn] - } - }); - - server.route({ - method: 'GET', - path: '/api/security/v1/users/{username}', - handler(request) { - const username = request.params.username; - return callWithRequest(request, 'shield.getUser', { username }).then( - (response) => { - if (response[username]) return response[username]; - throw Boom.notFound(); - }, - wrapError); - }, - config: { - pre: [routePreCheckLicenseFn] - } - }); - - server.route({ - method: 'POST', - path: '/api/security/v1/users/{username}', - handler(request) { - const username = request.params.username; - const body = _(request.payload).omit(['username', 'enabled']).omit(_.isNull); - return callWithRequest(request, 'shield.putUser', { username, body }).then( - () => request.payload, - wrapError); - }, - config: { - validate: { - payload: userSchema - }, - pre: [routePreCheckLicenseFn] - } - }); - - server.route({ - method: 'DELETE', - path: '/api/security/v1/users/{username}', - handler(request, h) { - const username = request.params.username; - return callWithRequest(request, 'shield.deleteUser', { username }).then( - () => h.response().code(204), - wrapError); - }, - config: { - pre: [routePreCheckLicenseFn] - } - }); - - server.route({ - method: 'POST', - path: '/api/security/v1/users/{username}/password', - async handler(request, h) { - const username = request.params.username; - const { password, newPassword } = request.payload; - const isCurrentUser = username === request.auth.credentials.username; - - // We should prefer `token` over `basic` if possible. - const providerToLoginWith = config.authc.providers.includes('token') - ? 'token' - : 'basic'; - - // If user tries to change own password, let's check if old password is valid first by trying - // to login. - if (isCurrentUser) { - const authenticationResult = await login(KibanaRequest.from(request), { - provider: providerToLoginWith, - value: { username, password }, - // We shouldn't alter authentication state just yet. - stateless: true, - }); - - if (!authenticationResult.succeeded()) { - return Boom.forbidden(authenticationResult.error); - } - } - - try { - const body = { password: newPassword }; - await callWithRequest(request, 'shield.changePassword', { username, body }); - - // Now we authenticate user with the new password again updating current session if any. - if (isCurrentUser) { - const authenticationResult = await login(KibanaRequest.from(request), { - provider: providerToLoginWith, - value: { username, password: newPassword } - }); - - if (!authenticationResult.succeeded()) { - return Boom.unauthorized((authenticationResult.error)); - } - } - } catch(err) { - return wrapError(err); - } - - return h.response().code(204); - }, - config: { - validate: { - payload: Joi.object({ - password: Joi.string(), - newPassword: Joi.string().required() - }) - }, - pre: [routePreCheckLicenseFn] - } - }); -} diff --git a/x-pack/legacy/plugins/siem/cypress/integration/lib/login/helpers.ts b/x-pack/legacy/plugins/siem/cypress/integration/lib/login/helpers.ts index 8a9477ad67901..b2b8ce7b9c000 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/lib/login/helpers.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/lib/login/helpers.ts @@ -39,7 +39,7 @@ const ELASTICSEARCH_PASSWORD = 'ELASTICSEARCH_PASSWORD'; /** * The Kibana server endpoint used for authentication */ -const LOGIN_API_ENDPOINT = '/api/security/v1/login'; +const LOGIN_API_ENDPOINT = '/internal/security/login'; /** * Authenticates with Kibana using, if specified, credentials specified by @@ -68,7 +68,7 @@ const credentialsProvidedByEnvironment = (): boolean => * Authenticates with Kibana by reading credentials from the * `CYPRESS_ELASTICSEARCH_USERNAME` and `CYPRESS_ELASTICSEARCH_PASSWORD` * environment variables, and POSTing the username and password directly to - * Kibana's `security/v1/login` endpoint, bypassing the login page (for speed). + * Kibana's `/internal/security/login` endpoint, bypassing the login page (for speed). */ const loginViaEnvironmentCredentials = () => { cy.log( @@ -90,7 +90,7 @@ const loginViaEnvironmentCredentials = () => { /** * Authenticates with Kibana by reading credentials from the * `kibana.dev.yml` file and POSTing the username and password directly to - * Kibana's `security/v1/login` endpoint, bypassing the login page (for speed). + * Kibana's `/internal/security/login` endpoint, bypassing the login page (for speed). */ const loginViaConfig = () => { cy.log( diff --git a/x-pack/legacy/server/lib/esjs_shield_plugin.js b/x-pack/legacy/server/lib/esjs_shield_plugin.js deleted file mode 100644 index b6252035aa321..0000000000000 --- a/x-pack/legacy/server/lib/esjs_shield_plugin.js +++ /dev/null @@ -1,579 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -(function (root, factory) { - if (typeof define === 'function' && define.amd) { // eslint-disable-line no-undef - define([], factory); // eslint-disable-line no-undef - } else if (typeof exports === 'object') { - module.exports = factory(); - } else { - root.ElasticsearchShield = factory(); - } -}(this, function () { - return function addShieldApi(Client, config, components) { - const ca = components.clientAction.factory; - - Client.prototype.shield = components.clientAction.namespaceFactory(); - const shield = Client.prototype.shield.prototype; - - /** - * Perform a [shield.authenticate](Retrieve details about the currently authenticated user) request - * - * @param {Object} params - An object with parameters used to carry out this action - */ - shield.authenticate = ca({ - params: {}, - url: { - fmt: '/_security/_authenticate' - } - }); - - /** - * Perform a [shield.changePassword](Change the password of a user) request - * - * @param {Object} params - An object with parameters used to carry out this action - * @param {Boolean} params.refresh - Refresh the index after performing the operation - * @param {String} params.username - The username of the user to change the password for - */ - shield.changePassword = ca({ - params: { - refresh: { - type: 'boolean' - } - }, - urls: [ - { - fmt: '/_security/user/<%=username%>/_password', - req: { - username: { - type: 'string', - required: false - } - } - }, - { - fmt: '/_security/user/_password' - } - ], - needBody: true, - method: 'POST' - }); - - /** - * Perform a [shield.clearCachedRealms](Clears the internal user caches for specified realms) request - * - * @param {Object} params - An object with parameters used to carry out this action - * @param {String} params.usernames - Comma-separated list of usernames to clear from the cache - * @param {String} params.realms - Comma-separated list of realms to clear - */ - shield.clearCachedRealms = ca({ - params: { - usernames: { - type: 'string', - required: false - } - }, - url: { - fmt: '/_security/realm/<%=realms%>/_clear_cache', - req: { - realms: { - type: 'string', - required: true - } - } - }, - method: 'POST' - }); - - /** - * Perform a [shield.clearCachedRoles](Clears the internal caches for specified roles) request - * - * @param {Object} params - An object with parameters used to carry out this action - * @param {String} params.name - Role name - */ - shield.clearCachedRoles = ca({ - params: {}, - url: { - fmt: '/_security/role/<%=name%>/_clear_cache', - req: { - name: { - type: 'string', - required: true - } - } - }, - method: 'POST' - }); - - /** - * Perform a [shield.deleteRole](Remove a role from the native shield realm) request - * - * @param {Object} params - An object with parameters used to carry out this action - * @param {Boolean} params.refresh - Refresh the index after performing the operation - * @param {String} params.name - Role name - */ - shield.deleteRole = ca({ - params: { - refresh: { - type: 'boolean' - } - }, - url: { - fmt: '/_security/role/<%=name%>', - req: { - name: { - type: 'string', - required: true - } - } - }, - method: 'DELETE' - }); - - /** - * Perform a [shield.deleteUser](Remove a user from the native shield realm) request - * - * @param {Object} params - An object with parameters used to carry out this action - * @param {Boolean} params.refresh - Refresh the index after performing the operation - * @param {String} params.username - username - */ - shield.deleteUser = ca({ - params: { - refresh: { - type: 'boolean' - } - }, - url: { - fmt: '/_security/user/<%=username%>', - req: { - username: { - type: 'string', - required: true - } - } - }, - method: 'DELETE' - }); - - /** - * Perform a [shield.getRole](Retrieve one or more roles from the native shield realm) request - * - * @param {Object} params - An object with parameters used to carry out this action - * @param {String} params.name - Role name - */ - shield.getRole = ca({ - params: {}, - urls: [ - { - fmt: '/_security/role/<%=name%>', - req: { - name: { - type: 'string', - required: false - } - } - }, - { - fmt: '/_security/role' - } - ] - }); - - /** - * Perform a [shield.getUser](Retrieve one or more users from the native shield realm) request - * - * @param {Object} params - An object with parameters used to carry out this action - * @param {String, String[], Boolean} params.username - A comma-separated list of usernames - */ - shield.getUser = ca({ - params: {}, - urls: [ - { - fmt: '/_security/user/<%=username%>', - req: { - username: { - type: 'list', - required: false - } - } - }, - { - fmt: '/_security/user' - } - ] - }); - - /** - * Perform a [shield.putRole](Update or create a role for the native shield realm) request - * - * @param {Object} params - An object with parameters used to carry out this action - * @param {Boolean} params.refresh - Refresh the index after performing the operation - * @param {String} params.name - Role name - */ - shield.putRole = ca({ - params: { - refresh: { - type: 'boolean' - } - }, - url: { - fmt: '/_security/role/<%=name%>', - req: { - name: { - type: 'string', - required: true - } - } - }, - needBody: true, - method: 'PUT' - }); - - /** - * Perform a [shield.putUser](Update or create a user for the native shield realm) request - * - * @param {Object} params - An object with parameters used to carry out this action - * @param {Boolean} params.refresh - Refresh the index after performing the operation - * @param {String} params.username - The username of the User - */ - shield.putUser = ca({ - params: { - refresh: { - type: 'boolean' - } - }, - url: { - fmt: '/_security/user/<%=username%>', - req: { - username: { - type: 'string', - required: true - } - } - }, - needBody: true, - method: 'PUT' - }); - - /** - * Perform a [shield.getUserPrivileges](Retrieve a user's list of privileges) request - * - */ - shield.getUserPrivileges = ca({ - params: {}, - urls: [ - { - fmt: '/_security/user/_privileges' - } - ] - }); - - /** - * Asks Elasticsearch to prepare SAML authentication request to be sent to - * the 3rd-party SAML identity provider. - * - * @param {string} [acs] Optional assertion consumer service URL to use for SAML request or URL - * in the Kibana to which identity provider will post SAML response. Based on the ACS Elasticsearch - * will choose the right SAML realm. - * - * @param {string} [realm] Optional name of the Elasticsearch SAML realm to use to handle request. - * - * @returns {{realm: string, id: string, redirect: string}} Object that includes identifier - * of the SAML realm used to prepare authentication request, encrypted request token to be - * sent to Elasticsearch with SAML response and redirect URL to the identity provider that - * will be used to authenticate user. - */ - shield.samlPrepare = ca({ - method: 'POST', - needBody: true, - url: { - fmt: '/_security/saml/prepare' - } - }); - - /** - * Sends SAML response returned by identity provider to Elasticsearch for validation. - * - * @param {Array.} ids A list of encrypted request tokens returned within SAML - * preparation response. - * @param {string} content SAML response returned by identity provider. - * @param {string} [realm] Optional string used to identify the name of the OpenID Connect realm - * that should be used to authenticate request. - * - * @returns {{username: string, access_token: string, expires_in: number}} Object that - * includes name of the user, access token to use for any consequent requests that - * need to be authenticated and a number of seconds after which access token will expire. - */ - shield.samlAuthenticate = ca({ - method: 'POST', - needBody: true, - url: { - fmt: '/_security/saml/authenticate' - } - }); - - /** - * Invalidates SAML access token. - * - * @param {string} token SAML access token that needs to be invalidated. - * - * @returns {{redirect?: string}} - */ - shield.samlLogout = ca({ - method: 'POST', - needBody: true, - url: { - fmt: '/_security/saml/logout' - } - }); - - /** - * Invalidates SAML session based on Logout Request received from the Identity Provider. - * - * @param {string} queryString URL encoded query string provided by Identity Provider. - * @param {string} [acs] Optional assertion consumer service URL to use for SAML request or URL in the - * Kibana to which identity provider will post SAML response. Based on the ACS Elasticsearch - * will choose the right SAML realm to invalidate session. - * @param {string} [realm] Optional name of the Elasticsearch SAML realm to use to handle request. - * - * @returns {{redirect?: string}} - */ - shield.samlInvalidate = ca({ - method: 'POST', - needBody: true, - url: { - fmt: '/_security/saml/invalidate' - } - }); - - /** - * Asks Elasticsearch to prepare an OpenID Connect authentication request to be sent to - * the 3rd-party OpenID Connect provider. - * - * @param {string} realm The OpenID Connect realm name in Elasticsearch - * - * @returns {{state: string, nonce: string, redirect: string}} Object that includes two opaque parameters that need - * to be sent to Elasticsearch with the OpenID Connect response and redirect URL to the OpenID Connect provider that - * will be used to authenticate user. - */ - shield.oidcPrepare = ca({ - method: 'POST', - needBody: true, - url: { - fmt: '/_security/oidc/prepare' - } - }); - - /** - * Sends the URL to which the OpenID Connect Provider redirected the UA to Elasticsearch for validation. - * - * @param {string} state The state parameter that was returned by Elasticsearch in the - * preparation response. - * @param {string} nonce The nonce parameter that was returned by Elasticsearch in the - * preparation response. - * @param {string} redirect_uri The URL to where the UA was redirected by the OpenID Connect provider. - * @param {string} [realm] Optional string used to identify the name of the OpenID Connect realm - * that should be used to authenticate request. - * - * @returns {{username: string, access_token: string, refresh_token; string, expires_in: number}} Object that - * includes name of the user, access token to use for any consequent requests that - * need to be authenticated and a number of seconds after which access token will expire. - */ - shield.oidcAuthenticate = ca({ - method: 'POST', - needBody: true, - url: { - fmt: '/_security/oidc/authenticate' - } - }); - - /** - * Invalidates an access token and refresh token pair that was generated after an OpenID Connect authentication. - * - * @param {string} token An access token that was created by authenticating to an OpenID Connect realm and - * that needs to be invalidated. - * @param {string} refresh_token A refresh token that was created by authenticating to an OpenID Connect realm and - * that needs to be invalidated. - * - * @returns {{redirect?: string}} If the Elasticsearch OpenID Connect realm configuration and the - * OpenID Connect provider supports RP-initiated SLO, a URL to redirect the UA - */ - shield.oidcLogout = ca({ - method: 'POST', - needBody: true, - url: { - fmt: '/_security/oidc/logout' - } - }); - - /** - * Refreshes an access token. - * - * @param {string} grant_type Currently only "refresh_token" grant type is supported. - * @param {string} refresh_token One-time refresh token that will be exchanged to the new access/refresh token pair. - * - * @returns {{access_token: string, type: string, expires_in: number, refresh_token: string}} - */ - shield.getAccessToken = ca({ - method: 'POST', - needBody: true, - url: { - fmt: '/_security/oauth2/token' - } - }); - - /** - * Invalidates an access token. - * - * @param {string} token The access token to invalidate - * - * @returns {{created: boolean}} - */ - shield.deleteAccessToken = ca({ - method: 'DELETE', - needBody: true, - params: { - token: { - type: 'string' - } - }, - url: { - fmt: '/_security/oauth2/token' - } - }); - - shield.getPrivilege = ca({ - method: 'GET', - urls: [{ - fmt: '/_security/privilege/<%=privilege%>', - req: { - privilege: { - type: 'string', - required: false - } - } - }, { - fmt: '/_security/privilege' - }] - }); - - shield.deletePrivilege = ca({ - method: 'DELETE', - urls: [{ - fmt: '/_security/privilege/<%=application%>/<%=privilege%>', - req: { - application: { - type: 'string', - required: true - }, - privilege: { - type: 'string', - required: true - } - } - }] - }); - - shield.postPrivileges = ca({ - method: 'POST', - needBody: true, - url: { - fmt: '/_security/privilege' - } - }); - - shield.hasPrivileges = ca({ - method: 'POST', - needBody: true, - url: { - fmt: '/_security/user/_has_privileges' - } - }); - - shield.getBuiltinPrivileges = ca({ - params: {}, - urls: [ - { - fmt: '/_security/privilege/_builtin' - } - ] - }); - - /** - * Gets API keys in Elasticsearch - * @param {boolean} owner A boolean flag that can be used to query API keys owned by the currently authenticated user. - * Defaults to false. The realm_name or username parameters cannot be specified when this parameter is set to true as - * they are assumed to be the currently authenticated ones. - */ - shield.getAPIKeys = ca({ - method: 'GET', - urls: [{ - fmt: `/_security/api_key?owner=<%=owner%>`, - req: { - owner: { - type: 'boolean', - required: true - } - } - }] - }); - - /** - * Creates an API key in Elasticsearch for the current user. - * - * @param {string} name A name for this API key - * @param {object} role_descriptors Role descriptors for this API key, if not - * provided then permissions of authenticated user are applied. - * @param {string} [expiration] Optional expiration for the API key being generated. If expiration - * is not provided then the API keys do not expire. - * - * @returns {{id: string, name: string, api_key: string, expiration?: number}} - */ - shield.createAPIKey = ca({ - method: 'POST', - needBody: true, - url: { - fmt: '/_security/api_key', - }, - }); - - /** - * Invalidates an API key in Elasticsearch. - * - * @param {string} [id] An API key id. - * @param {string} [name] An API key name. - * @param {string} [realm_name] The name of an authentication realm. - * @param {string} [username] The username of a user. - * - * NOTE: While all parameters are optional, at least one of them is required. - * - * @returns {{invalidated_api_keys: string[], previously_invalidated_api_keys: string[], error_count: number, error_details?: object[]}} - */ - shield.invalidateAPIKey = ca({ - method: 'DELETE', - needBody: true, - url: { - fmt: '/_security/api_key', - }, - }); - - /** - * Gets an access token in exchange to the certificate chain for the target subject distinguished name. - * - * @param {string[]} x509_certificate_chain An ordered array of base64-encoded (Section 4 of RFC4648 - not - * base64url-encoded) DER PKIX certificate values. - * - * @returns {{access_token: string, type: string, expires_in: number}} - */ - shield.delegatePKI = ca({ - method: 'POST', - needBody: true, - url: { - fmt: '/_security/delegate_pki', - }, - }); - }; -})); diff --git a/x-pack/legacy/server/lib/get_client_shield.ts b/x-pack/legacy/server/lib/get_client_shield.ts deleted file mode 100644 index 1f68c2e6d3466..0000000000000 --- a/x-pack/legacy/server/lib/get_client_shield.ts +++ /dev/null @@ -1,14 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { once } from 'lodash'; -import { Legacy } from 'kibana'; -// @ts-ignore -import esShield from './esjs_shield_plugin'; - -export const getClient = once((server: Legacy.Server) => { - return server.plugins.elasticsearch.createCluster('security', { plugins: [esShield] }); -}); diff --git a/x-pack/legacy/plugins/security/common/model/api_key.ts b/x-pack/plugins/security/common/model/api_key.ts similarity index 100% rename from x-pack/legacy/plugins/security/common/model/api_key.ts rename to x-pack/plugins/security/common/model/api_key.ts diff --git a/x-pack/plugins/security/common/model/index.ts b/x-pack/plugins/security/common/model/index.ts index c6ccd2518d261..226ea3b70afe2 100644 --- a/x-pack/plugins/security/common/model/index.ts +++ b/x-pack/plugins/security/common/model/index.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +export { ApiKey, ApiKeyToInvalidate } from './api_key'; export { User, EditUser, getUserDisplayName } from './user'; export { AuthenticatedUser, canUserChangePassword } from './authenticated_user'; export { BuiltinESPrivileges } from './builtin_es_privileges'; diff --git a/x-pack/plugins/security/server/authentication/providers/oidc.test.ts b/x-pack/plugins/security/server/authentication/providers/oidc.test.ts index c1d7dcca4c78f..ad7eab76db088 100644 --- a/x-pack/plugins/security/server/authentication/providers/oidc.test.ts +++ b/x-pack/plugins/security/server/authentication/providers/oidc.test.ts @@ -42,7 +42,7 @@ describe('OIDCAuthenticationProvider', () => { describe('`login` method', () => { it('redirects third party initiated login attempts to the OpenId Connect Provider.', async () => { - const request = httpServerMock.createKibanaRequest({ path: '/api/security/v1/oidc' }); + const request = httpServerMock.createKibanaRequest({ path: '/api/security/oidc' }); mockOptions.client.callAsInternalUser.withArgs('shield.oidcPrepare').resolves({ state: 'statevalue', @@ -205,13 +205,13 @@ describe('OIDCAuthenticationProvider', () => { describe('authorization code flow', () => { defineAuthenticationFlowTests(() => ({ request: httpServerMock.createKibanaRequest({ - path: '/api/security/v1/oidc?code=somecodehere&state=somestatehere', + path: '/api/security/oidc?code=somecodehere&state=somestatehere', }), attempt: { flow: OIDCAuthenticationFlow.AuthorizationCode, - authenticationResponseURI: '/api/security/v1/oidc?code=somecodehere&state=somestatehere', + authenticationResponseURI: '/api/security/oidc?code=somecodehere&state=somestatehere', }, - expectedRedirectURI: '/api/security/v1/oidc?code=somecodehere&state=somestatehere', + expectedRedirectURI: '/api/security/oidc?code=somecodehere&state=somestatehere', })); }); @@ -219,14 +219,13 @@ describe('OIDCAuthenticationProvider', () => { defineAuthenticationFlowTests(() => ({ request: httpServerMock.createKibanaRequest({ path: - '/api/security/v1/oidc?authenticationResponseURI=http://kibana/api/security/v1/oidc/implicit#id_token=sometoken', + '/api/security/oidc?authenticationResponseURI=http://kibana/api/security/oidc/implicit#id_token=sometoken', }), attempt: { flow: OIDCAuthenticationFlow.Implicit, - authenticationResponseURI: - 'http://kibana/api/security/v1/oidc/implicit#id_token=sometoken', + authenticationResponseURI: 'http://kibana/api/security/oidc/implicit#id_token=sometoken', }, - expectedRedirectURI: 'http://kibana/api/security/v1/oidc/implicit#id_token=sometoken', + expectedRedirectURI: 'http://kibana/api/security/oidc/implicit#id_token=sometoken', })); }); }); diff --git a/x-pack/plugins/security/server/elasticsearch_client_plugin.ts b/x-pack/plugins/security/server/elasticsearch_client_plugin.ts new file mode 100644 index 0000000000000..60d947bd65863 --- /dev/null +++ b/x-pack/plugins/security/server/elasticsearch_client_plugin.ts @@ -0,0 +1,576 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export function elasticsearchClientPlugin(Client: any, config: unknown, components: any) { + const ca = components.clientAction.factory; + + Client.prototype.shield = components.clientAction.namespaceFactory(); + const shield = Client.prototype.shield.prototype; + + /** + * Perform a [shield.authenticate](Retrieve details about the currently authenticated user) request + * + * @param {Object} params - An object with parameters used to carry out this action + */ + shield.authenticate = ca({ + params: {}, + url: { + fmt: '/_security/_authenticate', + }, + }); + + /** + * Perform a [shield.changePassword](Change the password of a user) request + * + * @param {Object} params - An object with parameters used to carry out this action + * @param {Boolean} params.refresh - Refresh the index after performing the operation + * @param {String} params.username - The username of the user to change the password for + */ + shield.changePassword = ca({ + params: { + refresh: { + type: 'boolean', + }, + }, + urls: [ + { + fmt: '/_security/user/<%=username%>/_password', + req: { + username: { + type: 'string', + required: false, + }, + }, + }, + { + fmt: '/_security/user/_password', + }, + ], + needBody: true, + method: 'POST', + }); + + /** + * Perform a [shield.clearCachedRealms](Clears the internal user caches for specified realms) request + * + * @param {Object} params - An object with parameters used to carry out this action + * @param {String} params.usernames - Comma-separated list of usernames to clear from the cache + * @param {String} params.realms - Comma-separated list of realms to clear + */ + shield.clearCachedRealms = ca({ + params: { + usernames: { + type: 'string', + required: false, + }, + }, + url: { + fmt: '/_security/realm/<%=realms%>/_clear_cache', + req: { + realms: { + type: 'string', + required: true, + }, + }, + }, + method: 'POST', + }); + + /** + * Perform a [shield.clearCachedRoles](Clears the internal caches for specified roles) request + * + * @param {Object} params - An object with parameters used to carry out this action + * @param {String} params.name - Role name + */ + shield.clearCachedRoles = ca({ + params: {}, + url: { + fmt: '/_security/role/<%=name%>/_clear_cache', + req: { + name: { + type: 'string', + required: true, + }, + }, + }, + method: 'POST', + }); + + /** + * Perform a [shield.deleteRole](Remove a role from the native shield realm) request + * + * @param {Object} params - An object with parameters used to carry out this action + * @param {Boolean} params.refresh - Refresh the index after performing the operation + * @param {String} params.name - Role name + */ + shield.deleteRole = ca({ + params: { + refresh: { + type: 'boolean', + }, + }, + url: { + fmt: '/_security/role/<%=name%>', + req: { + name: { + type: 'string', + required: true, + }, + }, + }, + method: 'DELETE', + }); + + /** + * Perform a [shield.deleteUser](Remove a user from the native shield realm) request + * + * @param {Object} params - An object with parameters used to carry out this action + * @param {Boolean} params.refresh - Refresh the index after performing the operation + * @param {String} params.username - username + */ + shield.deleteUser = ca({ + params: { + refresh: { + type: 'boolean', + }, + }, + url: { + fmt: '/_security/user/<%=username%>', + req: { + username: { + type: 'string', + required: true, + }, + }, + }, + method: 'DELETE', + }); + + /** + * Perform a [shield.getRole](Retrieve one or more roles from the native shield realm) request + * + * @param {Object} params - An object with parameters used to carry out this action + * @param {String} params.name - Role name + */ + shield.getRole = ca({ + params: {}, + urls: [ + { + fmt: '/_security/role/<%=name%>', + req: { + name: { + type: 'string', + required: false, + }, + }, + }, + { + fmt: '/_security/role', + }, + ], + }); + + /** + * Perform a [shield.getUser](Retrieve one or more users from the native shield realm) request + * + * @param {Object} params - An object with parameters used to carry out this action + * @param {String, String[], Boolean} params.username - A comma-separated list of usernames + */ + shield.getUser = ca({ + params: {}, + urls: [ + { + fmt: '/_security/user/<%=username%>', + req: { + username: { + type: 'list', + required: false, + }, + }, + }, + { + fmt: '/_security/user', + }, + ], + }); + + /** + * Perform a [shield.putRole](Update or create a role for the native shield realm) request + * + * @param {Object} params - An object with parameters used to carry out this action + * @param {Boolean} params.refresh - Refresh the index after performing the operation + * @param {String} params.name - Role name + */ + shield.putRole = ca({ + params: { + refresh: { + type: 'boolean', + }, + }, + url: { + fmt: '/_security/role/<%=name%>', + req: { + name: { + type: 'string', + required: true, + }, + }, + }, + needBody: true, + method: 'PUT', + }); + + /** + * Perform a [shield.putUser](Update or create a user for the native shield realm) request + * + * @param {Object} params - An object with parameters used to carry out this action + * @param {Boolean} params.refresh - Refresh the index after performing the operation + * @param {String} params.username - The username of the User + */ + shield.putUser = ca({ + params: { + refresh: { + type: 'boolean', + }, + }, + url: { + fmt: '/_security/user/<%=username%>', + req: { + username: { + type: 'string', + required: true, + }, + }, + }, + needBody: true, + method: 'PUT', + }); + + /** + * Perform a [shield.getUserPrivileges](Retrieve a user's list of privileges) request + * + */ + shield.getUserPrivileges = ca({ + params: {}, + urls: [ + { + fmt: '/_security/user/_privileges', + }, + ], + }); + + /** + * Asks Elasticsearch to prepare SAML authentication request to be sent to + * the 3rd-party SAML identity provider. + * + * @param {string} [acs] Optional assertion consumer service URL to use for SAML request or URL + * in the Kibana to which identity provider will post SAML response. Based on the ACS Elasticsearch + * will choose the right SAML realm. + * + * @param {string} [realm] Optional name of the Elasticsearch SAML realm to use to handle request. + * + * @returns {{realm: string, id: string, redirect: string}} Object that includes identifier + * of the SAML realm used to prepare authentication request, encrypted request token to be + * sent to Elasticsearch with SAML response and redirect URL to the identity provider that + * will be used to authenticate user. + */ + shield.samlPrepare = ca({ + method: 'POST', + needBody: true, + url: { + fmt: '/_security/saml/prepare', + }, + }); + + /** + * Sends SAML response returned by identity provider to Elasticsearch for validation. + * + * @param {Array.} ids A list of encrypted request tokens returned within SAML + * preparation response. + * @param {string} content SAML response returned by identity provider. + * @param {string} [realm] Optional string used to identify the name of the OpenID Connect realm + * that should be used to authenticate request. + * + * @returns {{username: string, access_token: string, expires_in: number}} Object that + * includes name of the user, access token to use for any consequent requests that + * need to be authenticated and a number of seconds after which access token will expire. + */ + shield.samlAuthenticate = ca({ + method: 'POST', + needBody: true, + url: { + fmt: '/_security/saml/authenticate', + }, + }); + + /** + * Invalidates SAML access token. + * + * @param {string} token SAML access token that needs to be invalidated. + * + * @returns {{redirect?: string}} + */ + shield.samlLogout = ca({ + method: 'POST', + needBody: true, + url: { + fmt: '/_security/saml/logout', + }, + }); + + /** + * Invalidates SAML session based on Logout Request received from the Identity Provider. + * + * @param {string} queryString URL encoded query string provided by Identity Provider. + * @param {string} [acs] Optional assertion consumer service URL to use for SAML request or URL in the + * Kibana to which identity provider will post SAML response. Based on the ACS Elasticsearch + * will choose the right SAML realm to invalidate session. + * @param {string} [realm] Optional name of the Elasticsearch SAML realm to use to handle request. + * + * @returns {{redirect?: string}} + */ + shield.samlInvalidate = ca({ + method: 'POST', + needBody: true, + url: { + fmt: '/_security/saml/invalidate', + }, + }); + + /** + * Asks Elasticsearch to prepare an OpenID Connect authentication request to be sent to + * the 3rd-party OpenID Connect provider. + * + * @param {string} realm The OpenID Connect realm name in Elasticsearch + * + * @returns {{state: string, nonce: string, redirect: string}} Object that includes two opaque parameters that need + * to be sent to Elasticsearch with the OpenID Connect response and redirect URL to the OpenID Connect provider that + * will be used to authenticate user. + */ + shield.oidcPrepare = ca({ + method: 'POST', + needBody: true, + url: { + fmt: '/_security/oidc/prepare', + }, + }); + + /** + * Sends the URL to which the OpenID Connect Provider redirected the UA to Elasticsearch for validation. + * + * @param {string} state The state parameter that was returned by Elasticsearch in the + * preparation response. + * @param {string} nonce The nonce parameter that was returned by Elasticsearch in the + * preparation response. + * @param {string} redirect_uri The URL to where the UA was redirected by the OpenID Connect provider. + * @param {string} [realm] Optional string used to identify the name of the OpenID Connect realm + * that should be used to authenticate request. + * + * @returns {{username: string, access_token: string, refresh_token; string, expires_in: number}} Object that + * includes name of the user, access token to use for any consequent requests that + * need to be authenticated and a number of seconds after which access token will expire. + */ + shield.oidcAuthenticate = ca({ + method: 'POST', + needBody: true, + url: { + fmt: '/_security/oidc/authenticate', + }, + }); + + /** + * Invalidates an access token and refresh token pair that was generated after an OpenID Connect authentication. + * + * @param {string} token An access token that was created by authenticating to an OpenID Connect realm and + * that needs to be invalidated. + * @param {string} refresh_token A refresh token that was created by authenticating to an OpenID Connect realm and + * that needs to be invalidated. + * + * @returns {{redirect?: string}} If the Elasticsearch OpenID Connect realm configuration and the + * OpenID Connect provider supports RP-initiated SLO, a URL to redirect the UA + */ + shield.oidcLogout = ca({ + method: 'POST', + needBody: true, + url: { + fmt: '/_security/oidc/logout', + }, + }); + + /** + * Refreshes an access token. + * + * @param {string} grant_type Currently only "refresh_token" grant type is supported. + * @param {string} refresh_token One-time refresh token that will be exchanged to the new access/refresh token pair. + * + * @returns {{access_token: string, type: string, expires_in: number, refresh_token: string}} + */ + shield.getAccessToken = ca({ + method: 'POST', + needBody: true, + url: { + fmt: '/_security/oauth2/token', + }, + }); + + /** + * Invalidates an access token. + * + * @param {string} token The access token to invalidate + * + * @returns {{created: boolean}} + */ + shield.deleteAccessToken = ca({ + method: 'DELETE', + needBody: true, + params: { + token: { + type: 'string', + }, + }, + url: { + fmt: '/_security/oauth2/token', + }, + }); + + shield.getPrivilege = ca({ + method: 'GET', + urls: [ + { + fmt: '/_security/privilege/<%=privilege%>', + req: { + privilege: { + type: 'string', + required: false, + }, + }, + }, + { + fmt: '/_security/privilege', + }, + ], + }); + + shield.deletePrivilege = ca({ + method: 'DELETE', + urls: [ + { + fmt: '/_security/privilege/<%=application%>/<%=privilege%>', + req: { + application: { + type: 'string', + required: true, + }, + privilege: { + type: 'string', + required: true, + }, + }, + }, + ], + }); + + shield.postPrivileges = ca({ + method: 'POST', + needBody: true, + url: { + fmt: '/_security/privilege', + }, + }); + + shield.hasPrivileges = ca({ + method: 'POST', + needBody: true, + url: { + fmt: '/_security/user/_has_privileges', + }, + }); + + shield.getBuiltinPrivileges = ca({ + params: {}, + urls: [ + { + fmt: '/_security/privilege/_builtin', + }, + ], + }); + + /** + * Gets API keys in Elasticsearch + * @param {boolean} owner A boolean flag that can be used to query API keys owned by the currently authenticated user. + * Defaults to false. The realm_name or username parameters cannot be specified when this parameter is set to true as + * they are assumed to be the currently authenticated ones. + */ + shield.getAPIKeys = ca({ + method: 'GET', + urls: [ + { + fmt: `/_security/api_key?owner=<%=owner%>`, + req: { + owner: { + type: 'boolean', + required: true, + }, + }, + }, + ], + }); + + /** + * Creates an API key in Elasticsearch for the current user. + * + * @param {string} name A name for this API key + * @param {object} role_descriptors Role descriptors for this API key, if not + * provided then permissions of authenticated user are applied. + * @param {string} [expiration] Optional expiration for the API key being generated. If expiration + * is not provided then the API keys do not expire. + * + * @returns {{id: string, name: string, api_key: string, expiration?: number}} + */ + shield.createAPIKey = ca({ + method: 'POST', + needBody: true, + url: { + fmt: '/_security/api_key', + }, + }); + + /** + * Invalidates an API key in Elasticsearch. + * + * @param {string} [id] An API key id. + * @param {string} [name] An API key name. + * @param {string} [realm_name] The name of an authentication realm. + * @param {string} [username] The username of a user. + * + * NOTE: While all parameters are optional, at least one of them is required. + * + * @returns {{invalidated_api_keys: string[], previously_invalidated_api_keys: string[], error_count: number, error_details?: object[]}} + */ + shield.invalidateAPIKey = ca({ + method: 'DELETE', + needBody: true, + url: { + fmt: '/_security/api_key', + }, + }); + + /** + * Gets an access token in exchange to the certificate chain for the target subject distinguished name. + * + * @param {string[]} x509_certificate_chain An ordered array of base64-encoded (Section 4 of RFC4648 - not + * base64url-encoded) DER PKIX certificate values. + * + * @returns {{access_token: string, type: string, expires_in: number}} + */ + shield.delegatePKI = ca({ + method: 'POST', + needBody: true, + url: { + fmt: '/_security/delegate_pki', + }, + }); +} diff --git a/x-pack/plugins/security/server/errors.ts b/x-pack/plugins/security/server/errors.ts index e0c2918991696..b5f3667558f55 100644 --- a/x-pack/plugins/security/server/errors.ts +++ b/x-pack/plugins/security/server/errors.ts @@ -5,11 +5,25 @@ */ import Boom from 'boom'; +import { CustomHttpResponseOptions, ResponseError } from '../../../../src/core/server'; export function wrapError(error: any) { return Boom.boomify(error, { statusCode: getErrorStatusCode(error) }); } +/** + * Wraps error into error suitable for Core's custom error response. + * @param error Any error instance. + */ +export function wrapIntoCustomErrorResponse(error: any) { + const wrappedError = wrapError(error); + return { + body: wrappedError, + headers: wrappedError.output.headers, + statusCode: wrappedError.output.statusCode, + } as CustomHttpResponseOptions; +} + /** * Extracts error code from Boom and Elasticsearch "native" errors. * @param error Error instance to extract status code from. diff --git a/x-pack/plugins/security/server/index.ts b/x-pack/plugins/security/server/index.ts index ec43bbd95901a..e72e94e9cd94b 100644 --- a/x-pack/plugins/security/server/index.ts +++ b/x-pack/plugins/security/server/index.ts @@ -9,18 +9,8 @@ import { ConfigSchema } from './config'; import { Plugin } from './plugin'; // These exports are part of public Security plugin contract, any change in signature of exported -// functions or removal of exports should be considered as a breaking change. Ideally we should -// reduce number of such exports to zero and provide everything we want to expose via Setup/Start -// run-time contracts. -export { wrapError } from './errors'; -export { - canRedirectRequest, - AuthenticationResult, - DeauthenticationResult, - OIDCAuthenticationFlow, - CreateAPIKeyResult, -} from './authentication'; - +// functions or removal of exports should be considered as a breaking change. +export { AuthenticationResult, DeauthenticationResult, CreateAPIKeyResult } from './authentication'; export { PluginSetupContract } from './plugin'; export const config = { schema: ConfigSchema }; diff --git a/x-pack/plugins/security/server/plugin.test.ts b/x-pack/plugins/security/server/plugin.test.ts index 26788c3ef9230..0569f5f4de3a6 100644 --- a/x-pack/plugins/security/server/plugin.test.ts +++ b/x-pack/plugins/security/server/plugin.test.ts @@ -7,6 +7,7 @@ import { of } from 'rxjs'; import { ByteSizeValue } from '@kbn/config-schema'; import { IClusterClient, CoreSetup } from '../../../../src/core/server'; +import { elasticsearchClientPlugin } from './elasticsearch_client_plugin'; import { Plugin, PluginSetupDependencies } from './plugin'; import { coreMock, elasticsearchServiceMock } from '../../../../src/core/server/mocks'; @@ -48,12 +49,6 @@ describe('Security Plugin', () => { Object { "__legacyCompat": Object { "config": Object { - "authc": Object { - "providers": Array [ - "saml", - "token", - ], - }, "cookieName": "sid", "loginAssistanceMessage": undefined, "secureCookies": true, @@ -115,7 +110,7 @@ describe('Security Plugin', () => { expect(mockCoreSetup.elasticsearch.createClient).toHaveBeenCalledTimes(1); expect(mockCoreSetup.elasticsearch.createClient).toHaveBeenCalledWith('security', { - plugins: [require('../../../legacy/server/lib/esjs_shield_plugin')], + plugins: [elasticsearchClientPlugin], }); }); }); diff --git a/x-pack/plugins/security/server/plugin.ts b/x-pack/plugins/security/server/plugin.ts index 84d448331cef2..633b064da6d61 100644 --- a/x-pack/plugins/security/server/plugin.ts +++ b/x-pack/plugins/security/server/plugin.ts @@ -28,6 +28,7 @@ import { defineRoutes } from './routes'; import { SecurityLicenseService, SecurityLicense } from './licensing'; import { setupSavedObjects } from './saved_objects'; import { SecurityAuditLogger } from './audit'; +import { elasticsearchClientPlugin } from './elasticsearch_client_plugin'; export type SpacesService = Pick< SpacesPluginSetup['spacesService'], @@ -76,7 +77,8 @@ export interface PluginSetupContract { lifespan: number | null; }; secureCookies: boolean; - authc: { providers: string[] }; + cookieName: string; + loginAssistanceMessage: string; }>; }; } @@ -128,7 +130,7 @@ export class Plugin { .toPromise(); this.clusterClient = core.elasticsearch.createClient('security', { - plugins: [require('../../../legacy/server/lib/esjs_shield_plugin')], + plugins: [elasticsearchClientPlugin], }); const { license, update: updateLicense } = new SecurityLicenseService().setup(); @@ -213,7 +215,6 @@ export class Plugin { }, secureCookies: config.secureCookies, cookieName: config.cookieName, - authc: { providers: config.authc.providers }, }, }, }); diff --git a/x-pack/plugins/security/server/routes/api_keys/get.test.ts b/x-pack/plugins/security/server/routes/api_keys/get.test.ts new file mode 100644 index 0000000000000..2b2283edea2e8 --- /dev/null +++ b/x-pack/plugins/security/server/routes/api_keys/get.test.ts @@ -0,0 +1,160 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { kibanaResponseFactory, RequestHandlerContext } from '../../../../../../src/core/server'; +import { LICENSE_CHECK_STATE, LicenseCheck } from '../../../../licensing/server'; +import { defineGetApiKeysRoutes } from './get'; + +import { elasticsearchServiceMock, httpServerMock } from '../../../../../../src/core/server/mocks'; +import { routeDefinitionParamsMock } from '../index.mock'; +import Boom from 'boom'; + +interface TestOptions { + isAdmin?: boolean; + licenseCheckResult?: LicenseCheck; + apiResponse?: () => Promise; + asserts: { statusCode: number; result?: Record }; +} + +describe('Get API keys', () => { + const getApiKeysTest = ( + description: string, + { + licenseCheckResult = { state: LICENSE_CHECK_STATE.Valid }, + apiResponse, + asserts, + isAdmin = true, + }: TestOptions + ) => { + test(description, async () => { + const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); + + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(mockScopedClusterClient); + if (apiResponse) { + mockScopedClusterClient.callAsCurrentUser.mockImplementation(apiResponse); + } + + defineGetApiKeysRoutes(mockRouteDefinitionParams); + const [[, handler]] = mockRouteDefinitionParams.router.get.mock.calls; + + const headers = { authorization: 'foo' }; + const mockRequest = httpServerMock.createKibanaRequest({ + method: 'get', + path: '/internal/security/api_key', + query: { isAdmin: isAdmin.toString() }, + headers, + }); + const mockContext = ({ + licensing: { license: { check: jest.fn().mockReturnValue(licenseCheckResult) } }, + } as unknown) as RequestHandlerContext; + + const response = await handler(mockContext, mockRequest, kibanaResponseFactory); + expect(response.status).toBe(asserts.statusCode); + expect(response.payload).toEqual(asserts.result); + + if (apiResponse) { + expect(mockRouteDefinitionParams.clusterClient.asScoped).toHaveBeenCalledWith(mockRequest); + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith( + 'shield.getAPIKeys', + { owner: !isAdmin } + ); + } else { + expect(mockScopedClusterClient.callAsCurrentUser).not.toHaveBeenCalled(); + } + expect(mockContext.licensing.license.check).toHaveBeenCalledWith('security', 'basic'); + }); + }; + + describe('failure', () => { + getApiKeysTest('returns result of license checker', { + licenseCheckResult: { state: LICENSE_CHECK_STATE.Invalid, message: 'test forbidden message' }, + asserts: { statusCode: 403, result: { message: 'test forbidden message' } }, + }); + + const error = Boom.notAcceptable('test not acceptable message'); + getApiKeysTest('returns error from cluster client', { + apiResponse: async () => { + throw error; + }, + asserts: { statusCode: 406, result: error }, + }); + }); + + describe('success', () => { + getApiKeysTest('returns API keys', { + apiResponse: async () => ({ + api_keys: [ + { + id: 'YCLV7m0BJ3xI4hhWB648', + name: 'test-api-key', + creation: 1571670001452, + expiration: 1571756401452, + invalidated: false, + username: 'elastic', + realm: 'reserved', + }, + ], + }), + asserts: { + statusCode: 200, + result: { + apiKeys: [ + { + id: 'YCLV7m0BJ3xI4hhWB648', + name: 'test-api-key', + creation: 1571670001452, + expiration: 1571756401452, + invalidated: false, + username: 'elastic', + realm: 'reserved', + }, + ], + }, + }, + }); + getApiKeysTest('returns only valid API keys', { + apiResponse: async () => ({ + api_keys: [ + { + id: 'YCLV7m0BJ3xI4hhWB648', + name: 'test-api-key1', + creation: 1571670001452, + expiration: 1571756401452, + invalidated: true, + username: 'elastic', + realm: 'reserved', + }, + { + id: 'YCLV7m0BJ3xI4hhWB648', + name: 'test-api-key2', + creation: 1571670001452, + expiration: 1571756401452, + invalidated: false, + username: 'elastic', + realm: 'reserved', + }, + ], + }), + asserts: { + statusCode: 200, + result: { + apiKeys: [ + { + id: 'YCLV7m0BJ3xI4hhWB648', + name: 'test-api-key2', + creation: 1571670001452, + expiration: 1571756401452, + invalidated: false, + username: 'elastic', + realm: 'reserved', + }, + ], + }, + }, + }); + }); +}); diff --git a/x-pack/plugins/security/server/routes/api_keys/get.ts b/x-pack/plugins/security/server/routes/api_keys/get.ts new file mode 100644 index 0000000000000..6e98b4b098405 --- /dev/null +++ b/x-pack/plugins/security/server/routes/api_keys/get.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { ApiKey } from '../../../common/model'; +import { wrapIntoCustomErrorResponse } from '../../errors'; +import { createLicensedRouteHandler } from '../licensed_route_handler'; +import { RouteDefinitionParams } from '..'; + +export function defineGetApiKeysRoutes({ router, clusterClient }: RouteDefinitionParams) { + router.get( + { + path: '/internal/security/api_key', + validate: { + query: schema.object({ + // We don't use `schema.boolean` here, because all query string parameters are treated as + // strings and @kbn/config-schema doesn't coerce strings to booleans. + // + // A boolean flag that can be used to query API keys owned by the currently authenticated + // user. `false` means that only API keys of currently authenticated user will be returned. + isAdmin: schema.oneOf([schema.literal('true'), schema.literal('false')]), + }), + }, + }, + createLicensedRouteHandler(async (context, request, response) => { + try { + const isAdmin = request.query.isAdmin === 'true'; + const { api_keys: apiKeys } = (await clusterClient + .asScoped(request) + .callAsCurrentUser('shield.getAPIKeys', { owner: !isAdmin })) as { api_keys: ApiKey[] }; + + const validKeys = apiKeys.filter(({ invalidated }) => !invalidated); + + return response.ok({ body: { apiKeys: validKeys } }); + } catch (error) { + return response.customError(wrapIntoCustomErrorResponse(error)); + } + }) + ); +} diff --git a/x-pack/plugins/security/server/routes/api_keys/index.ts b/x-pack/plugins/security/server/routes/api_keys/index.ts new file mode 100644 index 0000000000000..d75eb1bcbe961 --- /dev/null +++ b/x-pack/plugins/security/server/routes/api_keys/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { defineGetApiKeysRoutes } from './get'; +import { defineCheckPrivilegesRoutes } from './privileges'; +import { defineInvalidateApiKeysRoutes } from './invalidate'; +import { RouteDefinitionParams } from '..'; + +export function defineApiKeysRoutes(params: RouteDefinitionParams) { + defineGetApiKeysRoutes(params); + defineCheckPrivilegesRoutes(params); + defineInvalidateApiKeysRoutes(params); +} diff --git a/x-pack/plugins/security/server/routes/api_keys/invalidate.test.ts b/x-pack/plugins/security/server/routes/api_keys/invalidate.test.ts new file mode 100644 index 0000000000000..4ea21bda5f743 --- /dev/null +++ b/x-pack/plugins/security/server/routes/api_keys/invalidate.test.ts @@ -0,0 +1,220 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Boom from 'boom'; +import { Type } from '@kbn/config-schema'; +import { kibanaResponseFactory, RequestHandlerContext } from '../../../../../../src/core/server'; +import { LICENSE_CHECK_STATE, LicenseCheck } from '../../../../licensing/server'; +import { defineInvalidateApiKeysRoutes } from './invalidate'; + +import { elasticsearchServiceMock, httpServerMock } from '../../../../../../src/core/server/mocks'; +import { routeDefinitionParamsMock } from '../index.mock'; + +interface TestOptions { + licenseCheckResult?: LicenseCheck; + apiResponses?: Array<() => Promise>; + payload?: Record; + asserts: { statusCode: number; result?: Record; apiArguments?: unknown[][] }; +} + +describe('Invalidate API keys', () => { + const postInvalidateTest = ( + description: string, + { + licenseCheckResult = { state: LICENSE_CHECK_STATE.Valid }, + apiResponses = [], + asserts, + payload, + }: TestOptions + ) => { + test(description, async () => { + const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(mockScopedClusterClient); + for (const apiResponse of apiResponses) { + mockScopedClusterClient.callAsCurrentUser.mockImplementationOnce(apiResponse); + } + + defineInvalidateApiKeysRoutes(mockRouteDefinitionParams); + const [[{ validate }, handler]] = mockRouteDefinitionParams.router.post.mock.calls; + + const headers = { authorization: 'foo' }; + const mockRequest = httpServerMock.createKibanaRequest({ + method: 'post', + path: '/internal/security/api_key/invalidate', + body: payload !== undefined ? (validate as any).body.validate(payload) : undefined, + headers, + }); + const mockContext = ({ + licensing: { license: { check: jest.fn().mockReturnValue(licenseCheckResult) } }, + } as unknown) as RequestHandlerContext; + + const response = await handler(mockContext, mockRequest, kibanaResponseFactory); + expect(response.status).toBe(asserts.statusCode); + expect(response.payload).toEqual(asserts.result); + + if (Array.isArray(asserts.apiArguments)) { + for (const apiArguments of asserts.apiArguments) { + expect(mockRouteDefinitionParams.clusterClient.asScoped).toHaveBeenCalledWith( + mockRequest + ); + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith(...apiArguments); + } + } else { + expect(mockScopedClusterClient.callAsCurrentUser).not.toHaveBeenCalled(); + } + expect(mockContext.licensing.license.check).toHaveBeenCalledWith('security', 'basic'); + }); + }; + + describe('request validation', () => { + let requestBodySchema: Type; + beforeEach(() => { + const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); + defineInvalidateApiKeysRoutes(mockRouteDefinitionParams); + + const [[{ validate }]] = mockRouteDefinitionParams.router.post.mock.calls; + requestBodySchema = (validate as any).body; + }); + + test('requires both isAdmin and apiKeys parameters', () => { + expect(() => + requestBodySchema.validate({}, {}, 'request body') + ).toThrowErrorMatchingInlineSnapshot( + `"[request body.apiKeys]: expected value of type [array] but got [undefined]"` + ); + + expect(() => + requestBodySchema.validate({ apiKeys: [] }, {}, 'request body') + ).toThrowErrorMatchingInlineSnapshot( + `"[request body.isAdmin]: expected value of type [boolean] but got [undefined]"` + ); + + expect(() => + requestBodySchema.validate({ apiKeys: {}, isAdmin: true }, {}, 'request body') + ).toThrowErrorMatchingInlineSnapshot( + `"[request body.apiKeys]: expected value of type [array] but got [Object]"` + ); + + expect(() => + requestBodySchema.validate( + { + apiKeys: [{ id: 'some-id', name: 'some-name', unknown: 'some-unknown' }], + isAdmin: true, + }, + {}, + 'request body' + ) + ).toThrowErrorMatchingInlineSnapshot( + `"[request body.apiKeys.0.unknown]: definition for this key is missing"` + ); + }); + }); + + describe('failure', () => { + postInvalidateTest('returns result of license checker', { + licenseCheckResult: { state: LICENSE_CHECK_STATE.Invalid, message: 'test forbidden message' }, + payload: { apiKeys: [{ id: 'si8If24B1bKsmSLTAhJV', name: 'my-api-key' }], isAdmin: true }, + asserts: { statusCode: 403, result: { message: 'test forbidden message' } }, + }); + + const error = Boom.notAcceptable('test not acceptable message'); + postInvalidateTest('returns error from cluster client', { + apiResponses: [ + async () => { + throw error; + }, + ], + payload: { + apiKeys: [{ id: 'si8If24B1bKsmSLTAhJV', name: 'my-api-key' }], + isAdmin: true, + }, + asserts: { + apiArguments: [['shield.invalidateAPIKey', { body: { id: 'si8If24B1bKsmSLTAhJV' } }]], + statusCode: 200, + result: { + itemsInvalidated: [], + errors: [ + { + id: 'si8If24B1bKsmSLTAhJV', + name: 'my-api-key', + error: Boom.notAcceptable('test not acceptable message'), + }, + ], + }, + }, + }); + }); + + describe('success', () => { + postInvalidateTest('invalidates API keys', { + apiResponses: [async () => null], + payload: { + apiKeys: [{ id: 'si8If24B1bKsmSLTAhJV', name: 'my-api-key' }], + isAdmin: true, + }, + asserts: { + apiArguments: [['shield.invalidateAPIKey', { body: { id: 'si8If24B1bKsmSLTAhJV' } }]], + statusCode: 200, + result: { + itemsInvalidated: [{ id: 'si8If24B1bKsmSLTAhJV', name: 'my-api-key' }], + errors: [], + }, + }, + }); + + postInvalidateTest('adds "owner" to body if isAdmin=false', { + apiResponses: [async () => null], + payload: { + apiKeys: [{ id: 'si8If24B1bKsmSLTAhJV', name: 'my-api-key' }], + isAdmin: false, + }, + asserts: { + apiArguments: [ + ['shield.invalidateAPIKey', { body: { id: 'si8If24B1bKsmSLTAhJV', owner: true } }], + ], + statusCode: 200, + result: { + itemsInvalidated: [{ id: 'si8If24B1bKsmSLTAhJV', name: 'my-api-key' }], + errors: [], + }, + }, + }); + + postInvalidateTest('returns only successful invalidation requests', { + apiResponses: [ + async () => null, + async () => { + throw Boom.notAcceptable('test not acceptable message'); + }, + ], + payload: { + apiKeys: [ + { id: 'si8If24B1bKsmSLTAhJV', name: 'my-api-key1' }, + { id: 'ab8If24B1bKsmSLTAhNC', name: 'my-api-key2' }, + ], + isAdmin: true, + }, + asserts: { + apiArguments: [ + ['shield.invalidateAPIKey', { body: { id: 'si8If24B1bKsmSLTAhJV' } }], + ['shield.invalidateAPIKey', { body: { id: 'ab8If24B1bKsmSLTAhNC' } }], + ], + statusCode: 200, + result: { + itemsInvalidated: [{ id: 'si8If24B1bKsmSLTAhJV', name: 'my-api-key1' }], + errors: [ + { + id: 'ab8If24B1bKsmSLTAhNC', + name: 'my-api-key2', + error: Boom.notAcceptable('test not acceptable message'), + }, + ], + }, + }, + }); + }); +}); diff --git a/x-pack/plugins/security/server/routes/api_keys/invalidate.ts b/x-pack/plugins/security/server/routes/api_keys/invalidate.ts new file mode 100644 index 0000000000000..cb86c1024ae9a --- /dev/null +++ b/x-pack/plugins/security/server/routes/api_keys/invalidate.ts @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { createLicensedRouteHandler } from '../licensed_route_handler'; +import { ApiKey } from '../../../common/model'; +import { wrapError, wrapIntoCustomErrorResponse } from '../../errors'; +import { RouteDefinitionParams } from '..'; + +interface ResponseType { + itemsInvalidated: Array>; + errors: Array & { error: Error }>; +} + +export function defineInvalidateApiKeysRoutes({ router, clusterClient }: RouteDefinitionParams) { + router.post( + { + path: '/internal/security/api_key/invalidate', + validate: { + body: schema.object({ + apiKeys: schema.arrayOf(schema.object({ id: schema.string(), name: schema.string() })), + isAdmin: schema.boolean(), + }), + }, + }, + createLicensedRouteHandler(async (context, request, response) => { + try { + const scopedClusterClient = clusterClient.asScoped(request); + + // Invalidate all API keys in parallel. + const invalidationResult = ( + await Promise.all( + request.body.apiKeys.map(async key => { + try { + const body: { id: string; owner?: boolean } = { id: key.id }; + if (!request.body.isAdmin) { + body.owner = true; + } + + // Send the request to invalidate the API key and return an error if it could not be deleted. + await scopedClusterClient.callAsCurrentUser('shield.invalidateAPIKey', { body }); + return { key, error: undefined }; + } catch (error) { + return { key, error: wrapError(error) }; + } + }) + ) + ).reduce( + (responseBody, { key, error }) => { + if (error) { + responseBody.errors.push({ ...key, error }); + } else { + responseBody.itemsInvalidated.push(key); + } + return responseBody; + }, + { itemsInvalidated: [], errors: [] } as ResponseType + ); + + return response.ok({ body: invalidationResult }); + } catch (error) { + return response.customError(wrapIntoCustomErrorResponse(error)); + } + }) + ); +} diff --git a/x-pack/plugins/security/server/routes/api_keys/privileges.test.ts b/x-pack/plugins/security/server/routes/api_keys/privileges.test.ts new file mode 100644 index 0000000000000..866e455063bdc --- /dev/null +++ b/x-pack/plugins/security/server/routes/api_keys/privileges.test.ts @@ -0,0 +1,187 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Boom from 'boom'; +import { LICENSE_CHECK_STATE, LicenseCheck } from '../../../../licensing/server'; +import { kibanaResponseFactory, RequestHandlerContext } from '../../../../../../src/core/server'; + +import { elasticsearchServiceMock, httpServerMock } from '../../../../../../src/core/server/mocks'; +import { routeDefinitionParamsMock } from '../index.mock'; +import { defineCheckPrivilegesRoutes } from './privileges'; + +interface TestOptions { + licenseCheckResult?: LicenseCheck; + apiResponses?: Array<() => Promise>; + asserts: { statusCode: number; result?: Record; apiArguments?: unknown[][] }; +} + +describe('Check API keys privileges', () => { + const getPrivilegesTest = ( + description: string, + { + licenseCheckResult = { state: LICENSE_CHECK_STATE.Valid }, + apiResponses = [], + asserts, + }: TestOptions + ) => { + test(description, async () => { + const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(mockScopedClusterClient); + for (const apiResponse of apiResponses) { + mockScopedClusterClient.callAsCurrentUser.mockImplementationOnce(apiResponse); + } + + defineCheckPrivilegesRoutes(mockRouteDefinitionParams); + const [[, handler]] = mockRouteDefinitionParams.router.get.mock.calls; + + const headers = { authorization: 'foo' }; + const mockRequest = httpServerMock.createKibanaRequest({ + method: 'get', + path: '/internal/security/api_key/privileges', + headers, + }); + const mockContext = ({ + licensing: { license: { check: jest.fn().mockReturnValue(licenseCheckResult) } }, + } as unknown) as RequestHandlerContext; + + const response = await handler(mockContext, mockRequest, kibanaResponseFactory); + expect(response.status).toBe(asserts.statusCode); + expect(response.payload).toEqual(asserts.result); + + if (Array.isArray(asserts.apiArguments)) { + for (const apiArguments of asserts.apiArguments) { + expect(mockRouteDefinitionParams.clusterClient.asScoped).toHaveBeenCalledWith( + mockRequest + ); + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith(...apiArguments); + } + } else { + expect(mockScopedClusterClient.callAsCurrentUser).not.toHaveBeenCalled(); + } + expect(mockContext.licensing.license.check).toHaveBeenCalledWith('security', 'basic'); + }); + }; + + describe('failure', () => { + getPrivilegesTest('returns result of license checker', { + licenseCheckResult: { state: LICENSE_CHECK_STATE.Invalid, message: 'test forbidden message' }, + asserts: { statusCode: 403, result: { message: 'test forbidden message' } }, + }); + + const error = Boom.notAcceptable('test not acceptable message'); + getPrivilegesTest('returns error from cluster client', { + apiResponses: [ + async () => { + throw error; + }, + async () => {}, + ], + asserts: { + apiArguments: [ + ['shield.hasPrivileges', { body: { cluster: ['manage_security', 'manage_api_key'] } }], + ['shield.getAPIKeys', { owner: true }], + ], + statusCode: 406, + result: error, + }, + }); + }); + + describe('success', () => { + getPrivilegesTest('returns areApiKeysEnabled and isAdmin', { + apiResponses: [ + async () => ({ + username: 'elastic', + has_all_requested: true, + cluster: { manage_api_key: true, manage_security: true }, + index: {}, + application: {}, + }), + async () => ({ + api_keys: [ + { + id: 'si8If24B1bKsmSLTAhJV', + name: 'my-api-key', + creation: 1574089261632, + expiration: 1574175661632, + invalidated: false, + username: 'elastic', + realm: 'reserved', + }, + ], + }), + ], + asserts: { + apiArguments: [ + ['shield.getAPIKeys', { owner: true }], + ['shield.hasPrivileges', { body: { cluster: ['manage_security', 'manage_api_key'] } }], + ], + statusCode: 200, + result: { areApiKeysEnabled: true, isAdmin: true }, + }, + }); + + getPrivilegesTest( + 'returns areApiKeysEnabled=false when getAPIKeys error message includes "api keys are not enabled"', + { + apiResponses: [ + async () => ({ + username: 'elastic', + has_all_requested: true, + cluster: { manage_api_key: true, manage_security: true }, + index: {}, + application: {}, + }), + async () => { + throw Boom.unauthorized('api keys are not enabled'); + }, + ], + asserts: { + apiArguments: [ + ['shield.getAPIKeys', { owner: true }], + ['shield.hasPrivileges', { body: { cluster: ['manage_security', 'manage_api_key'] } }], + ], + statusCode: 200, + result: { areApiKeysEnabled: false, isAdmin: true }, + }, + } + ); + + getPrivilegesTest('returns isAdmin=false when user has insufficient privileges', { + apiResponses: [ + async () => ({ + username: 'elastic', + has_all_requested: true, + cluster: { manage_api_key: false, manage_security: false }, + index: {}, + application: {}, + }), + async () => ({ + api_keys: [ + { + id: 'si8If24B1bKsmSLTAhJV', + name: 'my-api-key', + creation: 1574089261632, + expiration: 1574175661632, + invalidated: false, + username: 'elastic', + realm: 'reserved', + }, + ], + }), + ], + asserts: { + apiArguments: [ + ['shield.getAPIKeys', { owner: true }], + ['shield.hasPrivileges', { body: { cluster: ['manage_security', 'manage_api_key'] } }], + ], + statusCode: 200, + result: { areApiKeysEnabled: true, isAdmin: false }, + }, + }); + }); +}); diff --git a/x-pack/plugins/security/server/routes/api_keys/privileges.ts b/x-pack/plugins/security/server/routes/api_keys/privileges.ts new file mode 100644 index 0000000000000..216d1ef1bf4a4 --- /dev/null +++ b/x-pack/plugins/security/server/routes/api_keys/privileges.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { wrapIntoCustomErrorResponse } from '../../errors'; +import { createLicensedRouteHandler } from '../licensed_route_handler'; +import { RouteDefinitionParams } from '..'; + +export function defineCheckPrivilegesRoutes({ router, clusterClient }: RouteDefinitionParams) { + router.get( + { + path: '/internal/security/api_key/privileges', + validate: false, + }, + createLicensedRouteHandler(async (context, request, response) => { + try { + const scopedClusterClient = clusterClient.asScoped(request); + + const [ + { + cluster: { manage_security: manageSecurity, manage_api_key: manageApiKey }, + }, + { areApiKeysEnabled }, + ] = await Promise.all([ + scopedClusterClient.callAsCurrentUser('shield.hasPrivileges', { + body: { cluster: ['manage_security', 'manage_api_key'] }, + }), + scopedClusterClient.callAsCurrentUser('shield.getAPIKeys', { owner: true }).then( + // If the API returns a truthy result that means it's enabled. + result => ({ areApiKeysEnabled: !!result }), + // This is a brittle dependency upon message. Tracked by https://github.com/elastic/elasticsearch/issues/47759. + e => + e.message.includes('api keys are not enabled') + ? Promise.resolve({ areApiKeysEnabled: false }) + : Promise.reject(e) + ), + ]); + + return response.ok({ + body: { areApiKeysEnabled, isAdmin: manageSecurity || manageApiKey }, + }); + } catch (error) { + return response.customError(wrapIntoCustomErrorResponse(error)); + } + }) + ); +} diff --git a/x-pack/plugins/security/server/routes/authentication/basic.test.ts b/x-pack/plugins/security/server/routes/authentication/basic.test.ts new file mode 100644 index 0000000000000..8e24f99b1302d --- /dev/null +++ b/x-pack/plugins/security/server/routes/authentication/basic.test.ts @@ -0,0 +1,172 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Type } from '@kbn/config-schema'; +import { + IRouter, + kibanaResponseFactory, + RequestHandler, + RequestHandlerContext, + RouteConfig, +} from '../../../../../../src/core/server'; +import { LICENSE_CHECK_STATE } from '../../../../licensing/server'; +import { Authentication, AuthenticationResult } from '../../authentication'; +import { ConfigType } from '../../config'; +import { LegacyAPI } from '../../plugin'; +import { defineBasicRoutes } from './basic'; + +import { + elasticsearchServiceMock, + httpServerMock, + httpServiceMock, + loggingServiceMock, +} from '../../../../../../src/core/server/mocks'; +import { mockAuthenticatedUser } from '../../../common/model/authenticated_user.mock'; +import { authenticationMock } from '../../authentication/index.mock'; +import { authorizationMock } from '../../authorization/index.mock'; + +describe('Basic authentication routes', () => { + let router: jest.Mocked; + let authc: jest.Mocked; + let mockContext: RequestHandlerContext; + beforeEach(() => { + router = httpServiceMock.createRouter(); + authc = authenticationMock.create(); + + mockContext = ({ + licensing: { + license: { check: jest.fn().mockReturnValue({ check: LICENSE_CHECK_STATE.Valid }) }, + }, + } as unknown) as RequestHandlerContext; + + defineBasicRoutes({ + router, + clusterClient: elasticsearchServiceMock.createClusterClient(), + basePath: httpServiceMock.createBasePath(), + logger: loggingServiceMock.create().get(), + config: { authc: { providers: ['saml'] } } as ConfigType, + authc, + authz: authorizationMock.create(), + getLegacyAPI: () => ({ cspRules: 'test-csp-rule' } as LegacyAPI), + }); + }); + + describe('login', () => { + let routeHandler: RequestHandler; + let routeConfig: RouteConfig; + + const mockRequest = httpServerMock.createKibanaRequest({ + body: { username: 'user', password: 'password' }, + }); + + beforeEach(() => { + const [loginRouteConfig, loginRouteHandler] = router.post.mock.calls.find( + ([{ path }]) => path === '/internal/security/login' + )!; + + routeConfig = loginRouteConfig; + routeHandler = loginRouteHandler; + }); + + it('correctly defines route.', async () => { + expect(routeConfig.options).toEqual({ authRequired: false }); + expect(routeConfig.validate).toEqual({ + body: expect.any(Type), + query: undefined, + params: undefined, + }); + + const bodyValidator = (routeConfig.validate as any).body as Type; + expect(bodyValidator.validate({ username: 'user', password: 'password' })).toEqual({ + username: 'user', + password: 'password', + }); + + expect(() => bodyValidator.validate({})).toThrowErrorMatchingInlineSnapshot( + `"[username]: expected value of type [string] but got [undefined]"` + ); + expect(() => bodyValidator.validate({ username: 'user' })).toThrowErrorMatchingInlineSnapshot( + `"[password]: expected value of type [string] but got [undefined]"` + ); + expect(() => + bodyValidator.validate({ password: 'password' }) + ).toThrowErrorMatchingInlineSnapshot( + `"[username]: expected value of type [string] but got [undefined]"` + ); + expect(() => + bodyValidator.validate({ username: '', password: '' }) + ).toThrowErrorMatchingInlineSnapshot( + `"[username]: value is [] but it must have a minimum length of [1]."` + ); + expect(() => + bodyValidator.validate({ username: 'user', password: '' }) + ).toThrowErrorMatchingInlineSnapshot( + `"[password]: value is [] but it must have a minimum length of [1]."` + ); + expect(() => + bodyValidator.validate({ username: '', password: 'password' }) + ).toThrowErrorMatchingInlineSnapshot( + `"[username]: value is [] but it must have a minimum length of [1]."` + ); + }); + + it('returns 500 if authentication throws unhandled exception.', async () => { + const unhandledException = new Error('Something went wrong.'); + authc.login.mockRejectedValue(unhandledException); + + const response = await routeHandler(mockContext, mockRequest, kibanaResponseFactory); + + expect(response.status).toBe(500); + expect(response.payload).toEqual(unhandledException); + expect(authc.login).toHaveBeenCalledWith(mockRequest, { + provider: 'basic', + value: { username: 'user', password: 'password' }, + }); + }); + + it('returns 401 if authentication fails.', async () => { + const failureReason = new Error('Something went wrong.'); + authc.login.mockResolvedValue(AuthenticationResult.failed(failureReason)); + + const response = await routeHandler(mockContext, mockRequest, kibanaResponseFactory); + + expect(response.status).toBe(401); + expect(response.payload).toEqual(failureReason); + expect(authc.login).toHaveBeenCalledWith(mockRequest, { + provider: 'basic', + value: { username: 'user', password: 'password' }, + }); + }); + + it('returns 401 if authentication is not handled.', async () => { + authc.login.mockResolvedValue(AuthenticationResult.notHandled()); + + const response = await routeHandler(mockContext, mockRequest, kibanaResponseFactory); + + expect(response.status).toBe(401); + expect(response.payload).toEqual('Unauthorized'); + expect(authc.login).toHaveBeenCalledWith(mockRequest, { + provider: 'basic', + value: { username: 'user', password: 'password' }, + }); + }); + + describe('authentication succeeds', () => { + it(`returns user data`, async () => { + authc.login.mockResolvedValue(AuthenticationResult.succeeded(mockAuthenticatedUser())); + + const response = await routeHandler(mockContext, mockRequest, kibanaResponseFactory); + + expect(response.status).toBe(204); + expect(response.payload).toBeUndefined(); + expect(authc.login).toHaveBeenCalledWith(mockRequest, { + provider: 'basic', + value: { username: 'user', password: 'password' }, + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/security/server/routes/authentication/basic.ts b/x-pack/plugins/security/server/routes/authentication/basic.ts new file mode 100644 index 0000000000000..453dc1c4ea3b5 --- /dev/null +++ b/x-pack/plugins/security/server/routes/authentication/basic.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { wrapIntoCustomErrorResponse } from '../../errors'; +import { createLicensedRouteHandler } from '../licensed_route_handler'; +import { RouteDefinitionParams } from '..'; + +/** + * Defines routes required for Basic/Token authentication. + */ +export function defineBasicRoutes({ router, authc, config }: RouteDefinitionParams) { + router.post( + { + path: '/internal/security/login', + validate: { + body: schema.object({ + username: schema.string({ minLength: 1 }), + password: schema.string({ minLength: 1 }), + }), + }, + options: { authRequired: false }, + }, + createLicensedRouteHandler(async (context, request, response) => { + const { username, password } = request.body; + + try { + // We should prefer `token` over `basic` if possible. + const providerToLoginWith = config.authc.providers.includes('token') ? 'token' : 'basic'; + const authenticationResult = await authc.login(request, { + provider: providerToLoginWith, + value: { username, password }, + }); + + if (!authenticationResult.succeeded()) { + return response.unauthorized({ body: authenticationResult.error }); + } + + return response.noContent(); + } catch (error) { + return response.customError(wrapIntoCustomErrorResponse(error)); + } + }) + ); +} diff --git a/x-pack/plugins/security/server/routes/authentication/common.test.ts b/x-pack/plugins/security/server/routes/authentication/common.test.ts new file mode 100644 index 0000000000000..f57fb1d5a7d66 --- /dev/null +++ b/x-pack/plugins/security/server/routes/authentication/common.test.ts @@ -0,0 +1,202 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Type } from '@kbn/config-schema'; +import { + IRouter, + kibanaResponseFactory, + RequestHandler, + RequestHandlerContext, + RouteConfig, +} from '../../../../../../src/core/server'; +import { LICENSE_CHECK_STATE } from '../../../../licensing/server'; +import { Authentication, DeauthenticationResult } from '../../authentication'; +import { ConfigType } from '../../config'; +import { LegacyAPI } from '../../plugin'; +import { defineCommonRoutes } from './common'; + +import { + elasticsearchServiceMock, + httpServerMock, + httpServiceMock, + loggingServiceMock, +} from '../../../../../../src/core/server/mocks'; +import { mockAuthenticatedUser } from '../../../common/model/authenticated_user.mock'; +import { authenticationMock } from '../../authentication/index.mock'; +import { authorizationMock } from '../../authorization/index.mock'; + +describe('Common authentication routes', () => { + let router: jest.Mocked; + let authc: jest.Mocked; + let mockContext: RequestHandlerContext; + beforeEach(() => { + router = httpServiceMock.createRouter(); + authc = authenticationMock.create(); + + mockContext = ({ + licensing: { + license: { check: jest.fn().mockReturnValue({ check: LICENSE_CHECK_STATE.Valid }) }, + }, + } as unknown) as RequestHandlerContext; + + defineCommonRoutes({ + router, + clusterClient: elasticsearchServiceMock.createClusterClient(), + basePath: httpServiceMock.createBasePath(), + logger: loggingServiceMock.create().get(), + config: { authc: { providers: ['saml'] } } as ConfigType, + authc, + authz: authorizationMock.create(), + getLegacyAPI: () => ({ cspRules: 'test-csp-rule' } as LegacyAPI), + }); + }); + + describe('logout', () => { + let routeHandler: RequestHandler; + let routeConfig: RouteConfig; + + const mockRequest = httpServerMock.createKibanaRequest({ + body: { username: 'user', password: 'password' }, + }); + + beforeEach(() => { + const [loginRouteConfig, loginRouteHandler] = router.get.mock.calls.find( + ([{ path }]) => path === '/api/security/logout' + )!; + + routeConfig = loginRouteConfig; + routeHandler = loginRouteHandler; + }); + + it('correctly defines route.', async () => { + expect(routeConfig.options).toEqual({ authRequired: false }); + expect(routeConfig.validate).toEqual({ + body: undefined, + query: expect.any(Type), + params: undefined, + }); + + const queryValidator = (routeConfig.validate as any).query as Type; + expect(queryValidator.validate({ someRandomField: 'some-random' })).toEqual({ + someRandomField: 'some-random', + }); + expect(queryValidator.validate({})).toEqual({}); + expect(queryValidator.validate(undefined)).toEqual({}); + }); + + it('returns 500 if deauthentication throws unhandled exception.', async () => { + const unhandledException = new Error('Something went wrong.'); + authc.logout.mockRejectedValue(unhandledException); + + const response = await routeHandler(mockContext, mockRequest, kibanaResponseFactory); + + expect(response.status).toBe(500); + expect(response.payload).toEqual(unhandledException); + expect(authc.logout).toHaveBeenCalledWith(mockRequest); + }); + + it('returns 500 if authenticator fails to logout.', async () => { + const failureReason = new Error('Something went wrong.'); + authc.logout.mockResolvedValue(DeauthenticationResult.failed(failureReason)); + + const response = await routeHandler(mockContext, mockRequest, kibanaResponseFactory); + + expect(response.status).toBe(500); + expect(response.payload).toEqual(failureReason); + expect(authc.logout).toHaveBeenCalledWith(mockRequest); + }); + + it('returns 400 for AJAX requests that can not handle redirect.', async () => { + const mockAjaxRequest = httpServerMock.createKibanaRequest({ + headers: { 'kbn-xsrf': 'xsrf' }, + }); + + const response = await routeHandler(mockContext, mockAjaxRequest, kibanaResponseFactory); + + expect(response.status).toBe(400); + expect(response.payload).toEqual('Client should be able to process redirect response.'); + expect(authc.logout).not.toHaveBeenCalled(); + }); + + it('redirects user to the URL returned by authenticator.', async () => { + authc.logout.mockResolvedValue(DeauthenticationResult.redirectTo('https://custom.logout')); + + const response = await routeHandler(mockContext, mockRequest, kibanaResponseFactory); + + expect(response.status).toBe(302); + expect(response.payload).toBeUndefined(); + expect(response.options).toEqual({ headers: { location: 'https://custom.logout' } }); + expect(authc.logout).toHaveBeenCalledWith(mockRequest); + }); + + it('redirects user to the base path if deauthentication succeeds.', async () => { + authc.logout.mockResolvedValue(DeauthenticationResult.succeeded()); + + const response = await routeHandler(mockContext, mockRequest, kibanaResponseFactory); + + expect(response.status).toBe(302); + expect(response.payload).toBeUndefined(); + expect(response.options).toEqual({ headers: { location: '/mock-server-basepath/' } }); + expect(authc.logout).toHaveBeenCalledWith(mockRequest); + }); + + it('redirects user to the base path if deauthentication is not handled.', async () => { + authc.logout.mockResolvedValue(DeauthenticationResult.notHandled()); + + const response = await routeHandler(mockContext, mockRequest, kibanaResponseFactory); + + expect(response.status).toBe(302); + expect(response.payload).toBeUndefined(); + expect(response.options).toEqual({ headers: { location: '/mock-server-basepath/' } }); + expect(authc.logout).toHaveBeenCalledWith(mockRequest); + }); + }); + + describe('me', () => { + let routeHandler: RequestHandler; + let routeConfig: RouteConfig; + + const mockRequest = httpServerMock.createKibanaRequest({ + body: { username: 'user', password: 'password' }, + }); + + beforeEach(() => { + const [loginRouteConfig, loginRouteHandler] = router.get.mock.calls.find( + ([{ path }]) => path === '/internal/security/me' + )!; + + routeConfig = loginRouteConfig; + routeHandler = loginRouteHandler; + }); + + it('correctly defines route.', async () => { + expect(routeConfig.options).toBeUndefined(); + expect(routeConfig.validate).toBe(false); + }); + + it('returns 500 if cannot retrieve current user due to unhandled exception.', async () => { + const unhandledException = new Error('Something went wrong.'); + authc.getCurrentUser.mockRejectedValue(unhandledException); + + const response = await routeHandler(mockContext, mockRequest, kibanaResponseFactory); + + expect(response.status).toBe(500); + expect(response.payload).toEqual(unhandledException); + expect(authc.getCurrentUser).toHaveBeenCalledWith(mockRequest); + }); + + it('returns current user.', async () => { + const mockUser = mockAuthenticatedUser(); + authc.getCurrentUser.mockResolvedValue(mockUser); + + const response = await routeHandler(mockContext, mockRequest, kibanaResponseFactory); + + expect(response.status).toBe(200); + expect(response.payload).toEqual(mockUser); + expect(authc.getCurrentUser).toHaveBeenCalledWith(mockRequest); + }); + }); +}); diff --git a/x-pack/plugins/security/server/routes/authentication/common.ts b/x-pack/plugins/security/server/routes/authentication/common.ts new file mode 100644 index 0000000000000..cb4ec196459ee --- /dev/null +++ b/x-pack/plugins/security/server/routes/authentication/common.ts @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { canRedirectRequest } from '../../authentication'; +import { wrapIntoCustomErrorResponse } from '../../errors'; +import { createLicensedRouteHandler } from '../licensed_route_handler'; +import { RouteDefinitionParams } from '..'; + +/** + * Defines routes that are common to various authentication mechanisms. + */ +export function defineCommonRoutes({ router, authc, basePath, logger }: RouteDefinitionParams) { + // Generate two identical routes with new and deprecated URL and issue a warning if route with deprecated URL is ever used. + for (const path of ['/api/security/logout', '/api/security/v1/logout']) { + router.get( + { + path, + // Allow unknown query parameters as this endpoint can be hit by the 3rd-party with any + // set of query string parameters (e.g. SAML/OIDC logout request parameters). + validate: { query: schema.object({}, { allowUnknowns: true }) }, + options: { authRequired: false }, + }, + async (context, request, response) => { + const serverBasePath = basePath.serverBasePath; + if (path === '/api/security/v1/logout') { + logger.warn( + `The "${serverBasePath}${path}" URL is deprecated and will stop working in the next major version, please use "${serverBasePath}/api/security/logout" URL instead.`, + { tags: ['deprecation'] } + ); + } + + if (!canRedirectRequest(request)) { + return response.badRequest({ + body: 'Client should be able to process redirect response.', + }); + } + + try { + const deauthenticationResult = await authc.logout(request); + if (deauthenticationResult.failed()) { + return response.customError(wrapIntoCustomErrorResponse(deauthenticationResult.error)); + } + + return response.redirected({ + headers: { location: deauthenticationResult.redirectURL || `${serverBasePath}/` }, + }); + } catch (error) { + return response.customError(wrapIntoCustomErrorResponse(error)); + } + } + ); + } + + // Generate two identical routes with new and deprecated URL and issue a warning if route with deprecated URL is ever used. + for (const path of ['/internal/security/me', '/api/security/v1/me']) { + router.get( + { path, validate: false }, + createLicensedRouteHandler(async (context, request, response) => { + if (path === '/api/security/v1/me') { + logger.warn( + `The "${basePath.serverBasePath}${path}" endpoint is deprecated and will be removed in the next major version.`, + { tags: ['deprecation'] } + ); + } + + try { + return response.ok({ body: (await authc.getCurrentUser(request)) as any }); + } catch (error) { + return response.customError(wrapIntoCustomErrorResponse(error)); + } + }) + ); + } +} diff --git a/x-pack/plugins/security/server/routes/authentication/index.ts b/x-pack/plugins/security/server/routes/authentication/index.ts index 086647dcb3459..21f015cc23b68 100644 --- a/x-pack/plugins/security/server/routes/authentication/index.ts +++ b/x-pack/plugins/security/server/routes/authentication/index.ts @@ -6,11 +6,39 @@ import { defineSessionRoutes } from './session'; import { defineSAMLRoutes } from './saml'; +import { defineBasicRoutes } from './basic'; +import { defineCommonRoutes } from './common'; +import { defineOIDCRoutes } from './oidc'; import { RouteDefinitionParams } from '..'; +export function createCustomResourceResponse(body: string, contentType: string, cspRules: string) { + return { + body, + headers: { + 'content-type': contentType, + 'cache-control': 'private, no-cache, no-store', + 'content-security-policy': cspRules, + }, + statusCode: 200, + }; +} + export function defineAuthenticationRoutes(params: RouteDefinitionParams) { defineSessionRoutes(params); + defineCommonRoutes(params); + + if ( + params.config.authc.providers.includes('basic') || + params.config.authc.providers.includes('token') + ) { + defineBasicRoutes(params); + } + if (params.config.authc.providers.includes('saml')) { defineSAMLRoutes(params); } + + if (params.config.authc.providers.includes('oidc')) { + defineOIDCRoutes(params); + } } diff --git a/x-pack/plugins/security/server/routes/authentication/oidc.ts b/x-pack/plugins/security/server/routes/authentication/oidc.ts new file mode 100644 index 0000000000000..8483630763ae6 --- /dev/null +++ b/x-pack/plugins/security/server/routes/authentication/oidc.ts @@ -0,0 +1,274 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { i18n } from '@kbn/i18n'; +import { KibanaRequest, KibanaResponseFactory } from '../../../../../../src/core/server'; +import { OIDCAuthenticationFlow } from '../../authentication'; +import { createCustomResourceResponse } from '.'; +import { createLicensedRouteHandler } from '../licensed_route_handler'; +import { wrapIntoCustomErrorResponse } from '../../errors'; +import { ProviderLoginAttempt } from '../../authentication/providers/oidc'; +import { RouteDefinitionParams } from '..'; + +/** + * Defines routes required for SAML authentication. + */ +export function defineOIDCRoutes({ + router, + logger, + authc, + getLegacyAPI, + basePath, +}: RouteDefinitionParams) { + // Generate two identical routes with new and deprecated URL and issue a warning if route with deprecated URL is ever used. + for (const path of ['/api/security/oidc/implicit', '/api/security/v1/oidc/implicit']) { + /** + * The route should be configured as a redirect URI in OP when OpenID Connect implicit flow + * is used, so that we can extract authentication response from URL fragment and send it to + * the `/api/security/oidc` route. + */ + router.get( + { + path, + validate: false, + options: { authRequired: false }, + }, + (context, request, response) => { + const serverBasePath = basePath.serverBasePath; + if (path === '/api/security/v1/oidc/implicit') { + logger.warn( + `The "${serverBasePath}${path}" URL is deprecated and will stop working in the next major version, please use "${serverBasePath}/api/security/oidc/implicit" URL instead.`, + { tags: ['deprecation'] } + ); + } + return response.custom( + createCustomResourceResponse( + ` + + Kibana OpenID Connect Login + + + `, + 'text/html', + getLegacyAPI().cspRules + ) + ); + } + ); + } + + /** + * The route that accompanies `/api/security/oidc/implicit` and renders a JavaScript snippet + * that extracts fragment part from the URL and send it to the `/api/security/oidc` route. + * We need this separate endpoint because of default CSP policy that forbids inline scripts. + */ + router.get( + { + path: '/internal/security/oidc/implicit.js', + validate: false, + options: { authRequired: false }, + }, + (context, request, response) => { + const serverBasePath = basePath.serverBasePath; + return response.custom( + createCustomResourceResponse( + ` + window.location.replace( + '${serverBasePath}/api/security/oidc?authenticationResponseURI=' + encodeURIComponent(window.location.href) + ); + `, + 'text/javascript', + getLegacyAPI().cspRules + ) + ); + } + ); + + // Generate two identical routes with new and deprecated URL and issue a warning if route with deprecated URL is ever used. + for (const path of ['/api/security/oidc', '/api/security/v1/oidc']) { + router.get( + { + path, + validate: { + query: schema.object( + { + authenticationResponseURI: schema.maybe(schema.uri()), + code: schema.maybe(schema.string()), + error: schema.maybe(schema.string()), + error_description: schema.maybe(schema.string()), + error_uri: schema.maybe(schema.uri()), + iss: schema.maybe(schema.uri({ scheme: ['https'] })), + login_hint: schema.maybe(schema.string()), + target_link_uri: schema.maybe(schema.uri()), + state: schema.maybe(schema.string()), + }, + // The client MUST ignore unrecognized response parameters according to + // https://openid.net/specs/openid-connect-core-1_0.html#AuthResponseValidation and + // https://tools.ietf.org/html/rfc6749#section-4.1.2. + { allowUnknowns: true } + ), + }, + options: { authRequired: false }, + }, + createLicensedRouteHandler(async (context, request, response) => { + const serverBasePath = basePath.serverBasePath; + + // An HTTP GET request with a query parameter named `authenticationResponseURI` that includes URL fragment OpenID + // Connect Provider sent during implicit authentication flow to the Kibana own proxy page that extracted that URL + // fragment and put it into `authenticationResponseURI` query string parameter for this endpoint. See more details + // at https://openid.net/specs/openid-connect-core-1_0.html#ImplicitFlowAuth + let loginAttempt: ProviderLoginAttempt | undefined; + if (request.query.authenticationResponseURI) { + loginAttempt = { + flow: OIDCAuthenticationFlow.Implicit, + authenticationResponseURI: request.query.authenticationResponseURI, + }; + } else if (request.query.code || request.query.error) { + if (path === '/api/security/v1/oidc') { + logger.warn( + `The "${serverBasePath}${path}" URL is deprecated and will stop working in the next major version, please use "${serverBasePath}/api/security/oidc" URL instead.`, + { tags: ['deprecation'] } + ); + } + + // An HTTP GET request with a query parameter named `code` (or `error`) as the response to a successful (or + // failed) authentication from an OpenID Connect Provider during authorization code authentication flow. + // See more details at https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth. + loginAttempt = { + flow: OIDCAuthenticationFlow.AuthorizationCode, + // We pass the path only as we can't be sure of the full URL and Elasticsearch doesn't need it anyway. + authenticationResponseURI: request.url.path!, + }; + } else if (request.query.iss) { + logger.warn( + `The "${serverBasePath}${path}" URL is deprecated and will stop working in the next major version, please use "${serverBasePath}/api/security/oidc/initiate_login" URL for Third-Party Initiated login instead.`, + { tags: ['deprecation'] } + ); + // An HTTP GET request with a query parameter named `iss` as part of a 3rd party initiated authentication. + // See more details at https://openid.net/specs/openid-connect-core-1_0.html#ThirdPartyInitiatedLogin + loginAttempt = { + flow: OIDCAuthenticationFlow.InitiatedBy3rdParty, + iss: request.query.iss, + loginHint: request.query.login_hint, + }; + } + + if (!loginAttempt) { + return response.badRequest({ body: 'Unrecognized login attempt.' }); + } + + return performOIDCLogin(request, response, loginAttempt); + }) + ); + } + + // Generate two identical routes with new and deprecated URL and issue a warning if route with deprecated URL is ever used. + for (const path of ['/api/security/oidc/initiate_login', '/api/security/v1/oidc']) { + /** + * An HTTP POST request with the payload parameter named `iss` as part of a 3rd party initiated authentication. + * See more details at https://openid.net/specs/openid-connect-core-1_0.html#ThirdPartyInitiatedLogin + */ + router.post( + { + path, + validate: { + body: schema.object( + { + iss: schema.uri({ scheme: ['https'] }), + login_hint: schema.maybe(schema.string()), + target_link_uri: schema.maybe(schema.uri()), + }, + // Other parameters MAY be sent, if defined by extensions. Any parameters used that are not understood MUST + // be ignored by the Client according to https://openid.net/specs/openid-connect-core-1_0.html#ThirdPartyInitiatedLogin. + { allowUnknowns: true } + ), + }, + options: { authRequired: false }, + }, + createLicensedRouteHandler(async (context, request, response) => { + const serverBasePath = basePath.serverBasePath; + if (path === '/api/security/v1/oidc') { + logger.warn( + `The "${serverBasePath}${path}" URL is deprecated and will stop working in the next major version, please use "${serverBasePath}/api/security/oidc/initiate_login" URL for Third-Party Initiated login instead.`, + { tags: ['deprecation'] } + ); + } + + return performOIDCLogin(request, response, { + flow: OIDCAuthenticationFlow.InitiatedBy3rdParty, + iss: request.body.iss, + loginHint: request.body.login_hint, + }); + }) + ); + } + + /** + * An HTTP GET request with the query string parameter named `iss` as part of a 3rd party initiated authentication. + * See more details at https://openid.net/specs/openid-connect-core-1_0.html#ThirdPartyInitiatedLogin + */ + router.get( + { + path: '/api/security/oidc/initiate_login', + validate: { + query: schema.object( + { + iss: schema.uri({ scheme: ['https'] }), + login_hint: schema.maybe(schema.string()), + target_link_uri: schema.maybe(schema.uri()), + }, + // Other parameters MAY be sent, if defined by extensions. Any parameters used that are not understood MUST + // be ignored by the Client according to https://openid.net/specs/openid-connect-core-1_0.html#ThirdPartyInitiatedLogin. + { allowUnknowns: true } + ), + }, + options: { authRequired: false }, + }, + createLicensedRouteHandler(async (context, request, response) => { + return performOIDCLogin(request, response, { + flow: OIDCAuthenticationFlow.InitiatedBy3rdParty, + iss: request.query.iss, + loginHint: request.query.login_hint, + }); + }) + ); + + async function performOIDCLogin( + request: KibanaRequest, + response: KibanaResponseFactory, + loginAttempt: ProviderLoginAttempt + ) { + try { + // We handle the fact that the user might get redirected to Kibana while already having a session + // Return an error notifying the user they are already logged in. + const authenticationResult = await authc.login(request, { + provider: 'oidc', + value: loginAttempt, + }); + + if (authenticationResult.succeeded()) { + return response.forbidden({ + body: i18n.translate('xpack.security.conflictingSessionError', { + defaultMessage: + 'Sorry, you already have an active Kibana session. ' + + 'If you want to start a new one, please logout from the existing session first.', + }), + }); + } + + if (authenticationResult.redirected()) { + return response.redirected({ + headers: { location: authenticationResult.redirectURL! }, + }); + } + + return response.unauthorized({ body: authenticationResult.error }); + } catch (error) { + return response.customError(wrapIntoCustomErrorResponse(error)); + } + } +} diff --git a/x-pack/plugins/security/server/routes/authentication/saml.ts b/x-pack/plugins/security/server/routes/authentication/saml.ts index 61f40e583d24e..f724d0e7708be 100644 --- a/x-pack/plugins/security/server/routes/authentication/saml.ts +++ b/x-pack/plugins/security/server/routes/authentication/saml.ts @@ -6,6 +6,7 @@ import { schema } from '@kbn/config-schema'; import { SAMLLoginStep } from '../../authentication'; +import { createCustomResourceResponse } from '.'; import { RouteDefinitionParams } from '..'; /** @@ -18,18 +19,6 @@ export function defineSAMLRoutes({ getLegacyAPI, basePath, }: RouteDefinitionParams) { - function createCustomResourceResponse(body: string, contentType: string) { - return { - body, - headers: { - 'content-type': contentType, - 'cache-control': 'private, no-cache, no-store', - 'content-security-policy': getLegacyAPI().cspRules, - }, - statusCode: 200, - }; - } - router.get( { path: '/api/security/saml/capture-url-fragment', @@ -46,7 +35,8 @@ export function defineSAMLRoutes({ `, - 'text/html' + 'text/html', + getLegacyAPI().cspRules ) ); } @@ -66,7 +56,8 @@ export function defineSAMLRoutes({ '${basePath.serverBasePath}/api/security/saml/start?redirectURLFragment=' + encodeURIComponent(window.location.hash) ); `, - 'text/javascript' + 'text/javascript', + getLegacyAPI().cspRules ) ); } diff --git a/x-pack/plugins/security/server/routes/authorization/privileges/get.test.ts b/x-pack/plugins/security/server/routes/authorization/privileges/get.test.ts index 10fe0cdd67811..6afbad8e83ebe 100644 --- a/x-pack/plugins/security/server/routes/authorization/privileges/get.test.ts +++ b/x-pack/plugins/security/server/routes/authorization/privileges/get.test.ts @@ -81,7 +81,7 @@ describe('GET privileges', () => { }; describe('failure', () => { - getPrivilegesTest(`returns result of routePreCheckLicense`, { + getPrivilegesTest('returns result of license checker', { licenseCheckResult: { state: LICENSE_CHECK_STATE.Invalid, message: 'test forbidden message' }, asserts: { statusCode: 403, result: { message: 'test forbidden message' } }, }); diff --git a/x-pack/plugins/security/server/routes/authorization/roles/delete.test.ts b/x-pack/plugins/security/server/routes/authorization/roles/delete.test.ts index 61c5747550d75..22268245c3a44 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/delete.test.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/delete.test.ts @@ -73,16 +73,18 @@ describe('DELETE role', () => { }; describe('failure', () => { - deleteRoleTest(`returns result of license checker`, { + deleteRoleTest('returns result of license checker', { name: 'foo-role', licenseCheckResult: { state: LICENSE_CHECK_STATE.Invalid, message: 'test forbidden message' }, asserts: { statusCode: 403, result: { message: 'test forbidden message' } }, }); const error = Boom.notFound('test not found message'); - deleteRoleTest(`returns error from cluster client`, { + deleteRoleTest('returns error from cluster client', { name: 'foo-role', - apiResponse: () => Promise.reject(error), + apiResponse: async () => { + throw error; + }, asserts: { statusCode: 404, result: error }, }); }); diff --git a/x-pack/plugins/security/server/routes/authorization/roles/delete.ts b/x-pack/plugins/security/server/routes/authorization/roles/delete.ts index aab815fbe449f..de966d6f2a758 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/delete.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/delete.ts @@ -7,7 +7,7 @@ import { schema } from '@kbn/config-schema'; import { RouteDefinitionParams } from '../../index'; import { createLicensedRouteHandler } from '../../licensed_route_handler'; -import { wrapError } from '../../../errors'; +import { wrapIntoCustomErrorResponse } from '../../../errors'; export function defineDeleteRolesRoutes({ router, clusterClient }: RouteDefinitionParams) { router.delete( @@ -23,11 +23,7 @@ export function defineDeleteRolesRoutes({ router, clusterClient }: RouteDefiniti return response.noContent(); } catch (error) { - const wrappedError = wrapError(error); - return response.customError({ - body: wrappedError, - statusCode: wrappedError.output.statusCode, - }); + return response.customError(wrapIntoCustomErrorResponse(error)); } }) ); diff --git a/x-pack/plugins/security/server/routes/authorization/roles/get.test.ts b/x-pack/plugins/security/server/routes/authorization/roles/get.test.ts index 447d890605cc9..bb9edbd17b2c8 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/get.test.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/get.test.ts @@ -75,15 +75,17 @@ describe('GET role', () => { }; describe('failure', () => { - getRoleTest(`returns result of license check`, { + getRoleTest('returns result of license checker', { licenseCheckResult: { state: LICENSE_CHECK_STATE.Invalid, message: 'test forbidden message' }, asserts: { statusCode: 403, result: { message: 'test forbidden message' } }, }); const error = Boom.notAcceptable('test not acceptable message'); - getRoleTest(`returns error from cluster client`, { + getRoleTest('returns error from cluster client', { name: 'first_role', - apiResponse: () => Promise.reject(error), + apiResponse: async () => { + throw error; + }, asserts: { statusCode: 406, result: error }, }); diff --git a/x-pack/plugins/security/server/routes/authorization/roles/get.ts b/x-pack/plugins/security/server/routes/authorization/roles/get.ts index 1173d37cba64f..8c158bee1a15e 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/get.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/get.ts @@ -7,7 +7,7 @@ import { schema } from '@kbn/config-schema'; import { RouteDefinitionParams } from '../..'; import { createLicensedRouteHandler } from '../../licensed_route_handler'; -import { wrapError } from '../../../errors'; +import { wrapIntoCustomErrorResponse } from '../../../errors'; import { transformElasticsearchRoleToRole } from './model'; export function defineGetRolesRoutes({ router, authz, clusterClient }: RouteDefinitionParams) { @@ -35,11 +35,7 @@ export function defineGetRolesRoutes({ router, authz, clusterClient }: RouteDefi return response.notFound(); } catch (error) { - const wrappedError = wrapError(error); - return response.customError({ - body: wrappedError, - statusCode: wrappedError.output.statusCode, - }); + return response.customError(wrapIntoCustomErrorResponse(error)); } }) ); diff --git a/x-pack/plugins/security/server/routes/authorization/roles/get_all.test.ts b/x-pack/plugins/security/server/routes/authorization/roles/get_all.test.ts index 3bd85122c95d1..96f065d6c765a 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/get_all.test.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/get_all.test.ts @@ -67,14 +67,16 @@ describe('GET all roles', () => { }; describe('failure', () => { - getRolesTest(`returns result of license check`, { + getRolesTest('returns result of license checker', { licenseCheckResult: { state: LICENSE_CHECK_STATE.Invalid, message: 'test forbidden message' }, asserts: { statusCode: 403, result: { message: 'test forbidden message' } }, }); const error = Boom.notAcceptable('test not acceptable message'); - getRolesTest(`returns error from cluster client`, { - apiResponse: () => Promise.reject(error), + getRolesTest('returns error from cluster client', { + apiResponse: async () => { + throw error; + }, asserts: { statusCode: 406, result: error }, }); diff --git a/x-pack/plugins/security/server/routes/authorization/roles/get_all.ts b/x-pack/plugins/security/server/routes/authorization/roles/get_all.ts index 6ff431f0f8b6a..24be6c60e4b12 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/get_all.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/get_all.ts @@ -6,7 +6,7 @@ import { RouteDefinitionParams } from '../..'; import { createLicensedRouteHandler } from '../../licensed_route_handler'; -import { wrapError } from '../../../errors'; +import { wrapIntoCustomErrorResponse } from '../../../errors'; import { ElasticsearchRole, transformElasticsearchRoleToRole } from './model'; export function defineGetAllRolesRoutes({ router, authz, clusterClient }: RouteDefinitionParams) { @@ -37,11 +37,7 @@ export function defineGetAllRolesRoutes({ router, authz, clusterClient }: RouteD }), }); } catch (error) { - const wrappedError = wrapError(error); - return response.customError({ - body: wrappedError, - statusCode: wrappedError.output.statusCode, - }); + return response.customError(wrapIntoCustomErrorResponse(error)); } }) ); diff --git a/x-pack/plugins/security/server/routes/authorization/roles/put.test.ts b/x-pack/plugins/security/server/routes/authorization/roles/put.test.ts index cb80549df8417..d19debe692460 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/put.test.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/put.test.ts @@ -138,7 +138,7 @@ describe('PUT role', () => { }); describe('failure', () => { - putRoleTest(`returns result of license checker`, { + putRoleTest('returns result of license checker', { name: 'foo-role', licenseCheckResult: { state: LICENSE_CHECK_STATE.Invalid, message: 'test forbidden message' }, asserts: { statusCode: 403, result: { message: 'test forbidden message' } }, diff --git a/x-pack/plugins/security/server/routes/authorization/roles/put.ts b/x-pack/plugins/security/server/routes/authorization/roles/put.ts index e0245e7260446..5db83375afa96 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/put.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/put.ts @@ -7,7 +7,7 @@ import { schema } from '@kbn/config-schema'; import { RouteDefinitionParams } from '../../index'; import { createLicensedRouteHandler } from '../../licensed_route_handler'; -import { wrapError } from '../../../errors'; +import { wrapIntoCustomErrorResponse } from '../../../errors'; import { ElasticsearchRole, getPutPayloadSchema, @@ -52,11 +52,7 @@ export function definePutRolesRoutes({ router, authz, clusterClient }: RouteDefi return response.noContent(); } catch (error) { - const wrappedError = wrapError(error); - return response.customError({ - body: wrappedError, - statusCode: wrappedError.output.statusCode, - }); + return response.customError(wrapIntoCustomErrorResponse(error)); } }) ); diff --git a/x-pack/plugins/security/server/routes/index.ts b/x-pack/plugins/security/server/routes/index.ts index 73e276832f474..756eaa76e2c2e 100644 --- a/x-pack/plugins/security/server/routes/index.ts +++ b/x-pack/plugins/security/server/routes/index.ts @@ -12,6 +12,9 @@ import { LegacyAPI } from '../plugin'; import { defineAuthenticationRoutes } from './authentication'; import { defineAuthorizationRoutes } from './authorization'; +import { defineApiKeysRoutes } from './api_keys'; +import { defineIndicesRoutes } from './indices'; +import { defineUsersRoutes } from './users'; /** * Describes parameters used to define HTTP routes. @@ -30,4 +33,7 @@ export interface RouteDefinitionParams { export function defineRoutes(params: RouteDefinitionParams) { defineAuthenticationRoutes(params); defineAuthorizationRoutes(params); + defineApiKeysRoutes(params); + defineIndicesRoutes(params); + defineUsersRoutes(params); } diff --git a/x-pack/plugins/security/server/routes/indices/get_fields.ts b/x-pack/plugins/security/server/routes/indices/get_fields.ts new file mode 100644 index 0000000000000..64c3d4f7471ef --- /dev/null +++ b/x-pack/plugins/security/server/routes/indices/get_fields.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { RouteDefinitionParams } from '../index'; +import { wrapIntoCustomErrorResponse } from '../../errors'; + +export function defineGetFieldsRoutes({ router, clusterClient }: RouteDefinitionParams) { + router.get( + { + path: '/internal/security/fields/{query}', + validate: { params: schema.object({ query: schema.string() }) }, + }, + async (context, request, response) => { + try { + const indexMappings = (await clusterClient + .asScoped(request) + .callAsCurrentUser('indices.getFieldMapping', { + index: request.params.query, + fields: '*', + allowNoIndices: false, + includeDefaults: true, + })) as Record }>; + + // The flow is the following (see response format at https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-get-field-mapping.html): + // 1. Iterate over all matched indices. + // 2. Extract all the field names from the `mappings` field of the particular index. + // 3. Collect and flatten the list of the field names. + // 4. Use `Set` to get only unique field names. + return response.ok({ + body: Array.from( + new Set( + Object.values(indexMappings) + .map(indexMapping => Object.keys(indexMapping.mappings)) + .flat() + ) + ), + }); + } catch (error) { + return response.customError(wrapIntoCustomErrorResponse(error)); + } + } + ); +} diff --git a/x-pack/legacy/plugins/security/common/constants.ts b/x-pack/plugins/security/server/routes/indices/index.ts similarity index 54% rename from x-pack/legacy/plugins/security/common/constants.ts rename to x-pack/plugins/security/server/routes/indices/index.ts index 08e49ad995550..d6b5eccf0fada 100644 --- a/x-pack/legacy/plugins/security/common/constants.ts +++ b/x-pack/plugins/security/server/routes/indices/index.ts @@ -4,4 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -export const INTERNAL_API_BASE_PATH = '/internal/security'; +import { defineGetFieldsRoutes } from './get_fields'; +import { RouteDefinitionParams } from '..'; + +export function defineIndicesRoutes(params: RouteDefinitionParams) { + defineGetFieldsRoutes(params); +} diff --git a/x-pack/plugins/security/server/routes/users/change_password.test.ts b/x-pack/plugins/security/server/routes/users/change_password.test.ts new file mode 100644 index 0000000000000..9f88d28bc115f --- /dev/null +++ b/x-pack/plugins/security/server/routes/users/change_password.test.ts @@ -0,0 +1,207 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ObjectType } from '@kbn/config-schema'; +import { + IClusterClient, + IRouter, + IScopedClusterClient, + kibanaResponseFactory, + RequestHandler, + RequestHandlerContext, + RouteConfig, +} from '../../../../../../src/core/server'; +import { LICENSE_CHECK_STATE } from '../../../../licensing/server'; +import { Authentication, AuthenticationResult } from '../../authentication'; +import { ConfigType } from '../../config'; +import { LegacyAPI } from '../../plugin'; +import { defineChangeUserPasswordRoutes } from './change_password'; + +import { + elasticsearchServiceMock, + loggingServiceMock, + httpServiceMock, + httpServerMock, +} from '../../../../../../src/core/server/mocks'; +import { mockAuthenticatedUser } from '../../../common/model/authenticated_user.mock'; +import { authorizationMock } from '../../authorization/index.mock'; +import { authenticationMock } from '../../authentication/index.mock'; + +describe('Change password', () => { + let router: jest.Mocked; + let authc: jest.Mocked; + let mockClusterClient: jest.Mocked; + let mockScopedClusterClient: jest.Mocked; + let routeHandler: RequestHandler; + let routeConfig: RouteConfig; + let mockContext: RequestHandlerContext; + + function checkPasswordChangeAPICall( + username: string, + request: ReturnType + ) { + expect(mockClusterClient.asScoped).toHaveBeenCalledTimes(1); + expect(mockClusterClient.asScoped).toHaveBeenCalledWith(request); + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith( + 'shield.changePassword', + { username, body: { password: 'new-password' } } + ); + } + + beforeEach(() => { + router = httpServiceMock.createRouter(); + authc = authenticationMock.create(); + + authc.getCurrentUser.mockResolvedValue(mockAuthenticatedUser({ username: 'user' })); + authc.login.mockResolvedValue(AuthenticationResult.succeeded(mockAuthenticatedUser())); + + mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockClusterClient = elasticsearchServiceMock.createClusterClient(); + mockClusterClient.asScoped.mockReturnValue(mockScopedClusterClient); + + mockContext = ({ + licensing: { + license: { check: jest.fn().mockReturnValue({ check: LICENSE_CHECK_STATE.Valid }) }, + }, + } as unknown) as RequestHandlerContext; + + defineChangeUserPasswordRoutes({ + router, + clusterClient: mockClusterClient, + basePath: httpServiceMock.createBasePath(), + logger: loggingServiceMock.create().get(), + config: { authc: { providers: ['saml'] } } as ConfigType, + authc, + authz: authorizationMock.create(), + getLegacyAPI: () => ({ cspRules: 'test-csp-rule' } as LegacyAPI), + }); + + const [changePasswordRouteConfig, changePasswordRouteHandler] = router.post.mock.calls[0]; + routeConfig = changePasswordRouteConfig; + routeHandler = changePasswordRouteHandler; + }); + + it('correctly defines route.', async () => { + expect(routeConfig.path).toBe('/internal/security/users/{username}/password'); + + const paramsSchema = (routeConfig.validate as any).params as ObjectType; + expect(() => paramsSchema.validate({})).toThrowErrorMatchingInlineSnapshot( + `"[username]: expected value of type [string] but got [undefined]"` + ); + expect(() => paramsSchema.validate({ username: '' })).toThrowErrorMatchingInlineSnapshot( + `"[username]: value is [] but it must have a minimum length of [1]."` + ); + expect(() => + paramsSchema.validate({ username: 'a'.repeat(1025) }) + ).toThrowErrorMatchingInlineSnapshot( + `"[username]: value is [aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa] but it must have a maximum length of [1024]."` + ); + + const bodySchema = (routeConfig.validate as any).body as ObjectType; + expect(() => bodySchema.validate({})).toThrowErrorMatchingInlineSnapshot( + `"[newPassword]: expected value of type [string] but got [undefined]"` + ); + expect(() => bodySchema.validate({ newPassword: '' })).toThrowErrorMatchingInlineSnapshot( + `"[newPassword]: value is [] but it must have a minimum length of [1]."` + ); + expect(() => + bodySchema.validate({ newPassword: '123456', password: '' }) + ).toThrowErrorMatchingInlineSnapshot( + `"[password]: value is [] but it must have a minimum length of [1]."` + ); + }); + + describe('own password', () => { + const username = 'user'; + const mockRequest = httpServerMock.createKibanaRequest({ + params: { username }, + body: { password: 'old-password', newPassword: 'new-password' }, + }); + + it('returns 403 if old password is wrong.', async () => { + const loginFailureReason = new Error('Something went wrong.'); + authc.login.mockResolvedValue(AuthenticationResult.failed(loginFailureReason)); + + const response = await routeHandler(mockContext, mockRequest, kibanaResponseFactory); + + expect(response.status).toBe(403); + expect(response.payload).toEqual(loginFailureReason); + expect(mockScopedClusterClient.callAsCurrentUser).not.toHaveBeenCalled(); + }); + + it(`returns 401 if user can't authenticate with new password.`, async () => { + const loginFailureReason = new Error('Something went wrong.'); + authc.login.mockImplementation(async (request, attempt) => { + const credentials = attempt.value as { username: string; password: string }; + if (credentials.username === 'user' && credentials.password === 'new-password') { + return AuthenticationResult.failed(loginFailureReason); + } + + return AuthenticationResult.succeeded(mockAuthenticatedUser()); + }); + + const response = await routeHandler(mockContext, mockRequest, kibanaResponseFactory); + + expect(response.status).toBe(401); + expect(response.payload).toEqual(loginFailureReason); + + checkPasswordChangeAPICall(username, mockRequest); + }); + + it('returns 500 if password update request fails.', async () => { + const failureReason = new Error('Request failed.'); + mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(failureReason); + + const response = await routeHandler(mockContext, mockRequest, kibanaResponseFactory); + + expect(response.status).toBe(500); + expect(response.payload).toEqual(failureReason); + + checkPasswordChangeAPICall(username, mockRequest); + }); + + it('successfully changes own password if provided old password is correct.', async () => { + const response = await routeHandler(mockContext, mockRequest, kibanaResponseFactory); + + expect(response.status).toBe(204); + expect(response.payload).toBeUndefined(); + + checkPasswordChangeAPICall(username, mockRequest); + }); + }); + + describe('other user password', () => { + const username = 'target-user'; + const mockRequest = httpServerMock.createKibanaRequest({ + params: { username }, + body: { newPassword: 'new-password' }, + }); + + it('returns 500 if password update request fails.', async () => { + const failureReason = new Error('Request failed.'); + mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(failureReason); + + const response = await routeHandler(mockContext, mockRequest, kibanaResponseFactory); + + expect(response.status).toBe(500); + expect(response.payload).toEqual(failureReason); + expect(authc.login).not.toHaveBeenCalled(); + + checkPasswordChangeAPICall(username, mockRequest); + }); + + it('successfully changes user password.', async () => { + const response = await routeHandler(mockContext, mockRequest, kibanaResponseFactory); + + expect(response.status).toBe(204); + expect(response.payload).toBeUndefined(); + expect(authc.login).not.toHaveBeenCalled(); + + checkPasswordChangeAPICall(username, mockRequest); + }); + }); +}); diff --git a/x-pack/plugins/security/server/routes/users/change_password.ts b/x-pack/plugins/security/server/routes/users/change_password.ts new file mode 100644 index 0000000000000..b9d04b4bd1e0e --- /dev/null +++ b/x-pack/plugins/security/server/routes/users/change_password.ts @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { wrapIntoCustomErrorResponse } from '../../errors'; +import { createLicensedRouteHandler } from '../licensed_route_handler'; +import { RouteDefinitionParams } from '..'; + +export function defineChangeUserPasswordRoutes({ + authc, + router, + clusterClient, + config, +}: RouteDefinitionParams) { + router.post( + { + path: '/internal/security/users/{username}/password', + validate: { + params: schema.object({ username: schema.string({ minLength: 1, maxLength: 1024 }) }), + body: schema.object({ + password: schema.maybe(schema.string({ minLength: 1 })), + newPassword: schema.string({ minLength: 1 }), + }), + }, + }, + createLicensedRouteHandler(async (context, request, response) => { + const username = request.params.username; + const { password, newPassword } = request.body; + const isCurrentUser = username === (await authc.getCurrentUser(request))!.username; + + // We should prefer `token` over `basic` if possible. + const providerToLoginWith = config.authc.providers.includes('token') ? 'token' : 'basic'; + + // If user tries to change own password, let's check if old password is valid first by trying + // to login. + if (isCurrentUser) { + try { + const authenticationResult = await authc.login(request, { + provider: providerToLoginWith, + value: { username, password }, + // We shouldn't alter authentication state just yet. + stateless: true, + }); + + if (!authenticationResult.succeeded()) { + return response.forbidden({ body: authenticationResult.error }); + } + } catch (error) { + return response.customError(wrapIntoCustomErrorResponse(error)); + } + } + + try { + await clusterClient.asScoped(request).callAsCurrentUser('shield.changePassword', { + username, + body: { password: newPassword }, + }); + + // Now we authenticate user with the new password again updating current session if any. + if (isCurrentUser) { + const authenticationResult = await authc.login(request, { + provider: providerToLoginWith, + value: { username, password: newPassword }, + }); + + if (!authenticationResult.succeeded()) { + return response.unauthorized({ body: authenticationResult.error }); + } + } + + return response.noContent(); + } catch (error) { + return response.customError(wrapIntoCustomErrorResponse(error)); + } + }) + ); +} diff --git a/x-pack/plugins/security/server/routes/users/create_or_update.ts b/x-pack/plugins/security/server/routes/users/create_or_update.ts new file mode 100644 index 0000000000000..5a3e50bb11d5c --- /dev/null +++ b/x-pack/plugins/security/server/routes/users/create_or_update.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { wrapIntoCustomErrorResponse } from '../../errors'; +import { createLicensedRouteHandler } from '../licensed_route_handler'; +import { RouteDefinitionParams } from '..'; + +export function defineCreateOrUpdateUserRoutes({ router, clusterClient }: RouteDefinitionParams) { + router.post( + { + path: '/internal/security/users/{username}', + validate: { + params: schema.object({ username: schema.string({ minLength: 1, maxLength: 1024 }) }), + body: schema.object({ + username: schema.string({ minLength: 1, maxLength: 1024 }), + password: schema.maybe(schema.string({ minLength: 1 })), + roles: schema.arrayOf(schema.string({ minLength: 1 })), + full_name: schema.maybe(schema.oneOf([schema.literal(null), schema.string()])), + email: schema.maybe(schema.oneOf([schema.literal(null), schema.string()])), + metadata: schema.maybe(schema.recordOf(schema.string(), schema.any())), + enabled: schema.boolean({ defaultValue: true }), + }), + }, + }, + createLicensedRouteHandler(async (context, request, response) => { + try { + await clusterClient.asScoped(request).callAsCurrentUser('shield.putUser', { + username: request.params.username, + // Omit `username`, `enabled` and all fields with `null` value. + body: Object.fromEntries( + Object.entries(request.body).filter( + ([key, value]) => value !== null && key !== 'enabled' && key !== 'username' + ) + ), + }); + + return response.ok({ body: request.body }); + } catch (error) { + return response.customError(wrapIntoCustomErrorResponse(error)); + } + }) + ); +} diff --git a/x-pack/plugins/security/server/routes/users/delete.ts b/x-pack/plugins/security/server/routes/users/delete.ts new file mode 100644 index 0000000000000..99a8d5c18ab3d --- /dev/null +++ b/x-pack/plugins/security/server/routes/users/delete.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { RouteDefinitionParams } from '../index'; +import { wrapIntoCustomErrorResponse } from '../../errors'; +import { createLicensedRouteHandler } from '../licensed_route_handler'; + +export function defineDeleteUserRoutes({ router, clusterClient }: RouteDefinitionParams) { + router.delete( + { + path: '/internal/security/users/{username}', + validate: { + params: schema.object({ username: schema.string({ minLength: 1, maxLength: 1024 }) }), + }, + }, + createLicensedRouteHandler(async (context, request, response) => { + try { + await clusterClient + .asScoped(request) + .callAsCurrentUser('shield.deleteUser', { username: request.params.username }); + + return response.noContent(); + } catch (error) { + return response.customError(wrapIntoCustomErrorResponse(error)); + } + }) + ); +} diff --git a/x-pack/plugins/security/server/routes/users/get.ts b/x-pack/plugins/security/server/routes/users/get.ts new file mode 100644 index 0000000000000..0867910372546 --- /dev/null +++ b/x-pack/plugins/security/server/routes/users/get.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { wrapIntoCustomErrorResponse } from '../../errors'; +import { createLicensedRouteHandler } from '../licensed_route_handler'; +import { RouteDefinitionParams } from '..'; + +export function defineGetUserRoutes({ router, clusterClient }: RouteDefinitionParams) { + router.get( + { + path: '/internal/security/users/{username}', + validate: { + params: schema.object({ username: schema.string({ minLength: 1, maxLength: 1024 }) }), + }, + }, + createLicensedRouteHandler(async (context, request, response) => { + try { + const username = request.params.username; + const users = (await clusterClient + .asScoped(request) + .callAsCurrentUser('shield.getUser', { username })) as Record; + + if (!users[username]) { + return response.notFound(); + } + + return response.ok({ body: users[username] }); + } catch (error) { + return response.customError(wrapIntoCustomErrorResponse(error)); + } + }) + ); +} diff --git a/x-pack/plugins/security/server/routes/users/get_all.ts b/x-pack/plugins/security/server/routes/users/get_all.ts new file mode 100644 index 0000000000000..492ab27ab27ad --- /dev/null +++ b/x-pack/plugins/security/server/routes/users/get_all.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { RouteDefinitionParams } from '../index'; +import { wrapIntoCustomErrorResponse } from '../../errors'; +import { createLicensedRouteHandler } from '../licensed_route_handler'; + +export function defineGetAllUsersRoutes({ router, clusterClient }: RouteDefinitionParams) { + router.get( + { path: '/internal/security/users', validate: false }, + createLicensedRouteHandler(async (context, request, response) => { + try { + return response.ok({ + // Return only values since keys (user names) are already duplicated there. + body: Object.values( + await clusterClient.asScoped(request).callAsCurrentUser('shield.getUser') + ), + }); + } catch (error) { + return response.customError(wrapIntoCustomErrorResponse(error)); + } + }) + ); +} diff --git a/x-pack/plugins/security/server/routes/users/index.ts b/x-pack/plugins/security/server/routes/users/index.ts new file mode 100644 index 0000000000000..931af0734b416 --- /dev/null +++ b/x-pack/plugins/security/server/routes/users/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { RouteDefinitionParams } from '../index'; +import { defineGetUserRoutes } from './get'; +import { defineGetAllUsersRoutes } from './get_all'; +import { defineCreateOrUpdateUserRoutes } from './create_or_update'; +import { defineDeleteUserRoutes } from './delete'; +import { defineChangeUserPasswordRoutes } from './change_password'; + +export function defineUsersRoutes(params: RouteDefinitionParams) { + defineGetUserRoutes(params); + defineGetAllUsersRoutes(params); + defineCreateOrUpdateUserRoutes(params); + defineDeleteUserRoutes(params); + defineChangeUserPasswordRoutes(params); +} diff --git a/x-pack/test/api_integration/apis/security/basic_login.js b/x-pack/test/api_integration/apis/security/basic_login.js index 1d10b3f8803a5..cd85e6906d65e 100644 --- a/x-pack/test/api_integration/apis/security/basic_login.js +++ b/x-pack/test/api_integration/apis/security/basic_login.js @@ -32,7 +32,7 @@ export default function ({ getService }) { it('should reject API requests if client is not authenticated', async () => { await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .expect(401); }); @@ -41,24 +41,24 @@ export default function ({ getService }) { const wrongUsername = `wrong-${validUsername}`; const wrongPassword = `wrong-${validPassword}`; - await supertest.post('/api/security/v1/login') + await supertest.post('/internal/security/login') .set('kbn-xsrf', 'xxx') .send({ username: wrongUsername, password: wrongPassword }) .expect(401); - await supertest.post('/api/security/v1/login') + await supertest.post('/internal/security/login') .set('kbn-xsrf', 'xxx') .send({ username: validUsername, password: wrongPassword }) .expect(401); - await supertest.post('/api/security/v1/login') + await supertest.post('/internal/security/login') .set('kbn-xsrf', 'xxx') .send({ username: wrongUsername, password: validPassword }) .expect(401); }); it('should set authentication cookie for login with valid credentials', async () => { - const loginResponse = await supertest.post('/api/security/v1/login') + const loginResponse = await supertest.post('/internal/security/login') .set('kbn-xsrf', 'xxx') .send({ username: validUsername, password: validPassword }) .expect(204); @@ -77,17 +77,17 @@ export default function ({ getService }) { const wrongUsername = `wrong-${validUsername}`; const wrongPassword = `wrong-${validPassword}`; - await supertest.get('/api/security/v1/me') + await supertest.get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Authorization', `Basic ${Buffer.from(`${wrongUsername}:${wrongPassword}`).toString('base64')}`) .expect(401); - await supertest.get('/api/security/v1/me') + await supertest.get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Authorization', `Basic ${Buffer.from(`${validUsername}:${wrongPassword}`).toString('base64')}`) .expect(401); - await supertest.get('/api/security/v1/me') + await supertest.get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Authorization', `Basic ${Buffer.from(`${wrongUsername}:${validPassword}`).toString('base64')}`) .expect(401); @@ -95,7 +95,7 @@ export default function ({ getService }) { it('should allow access to the API with valid credentials in the header', async () => { const apiResponse = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Authorization', `Basic ${Buffer.from(`${validUsername}:${validPassword}`).toString('base64')}`) .expect(200); @@ -116,7 +116,7 @@ export default function ({ getService }) { describe('with session cookie', () => { let sessionCookie; beforeEach(async () => { - const loginResponse = await supertest.post('/api/security/v1/login') + const loginResponse = await supertest.post('/internal/security/login') .set('kbn-xsrf', 'xxx') .send({ username: validUsername, password: validPassword }) .expect(204); @@ -128,12 +128,12 @@ export default function ({ getService }) { // There is no session cookie provided and no server side session should have // been established, so request should be rejected. await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .expect(401); const apiResponse = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', sessionCookie.cookieString()) .expect(200); @@ -153,7 +153,7 @@ export default function ({ getService }) { it('should extend cookie on every successful non-system API call', async () => { const apiResponseOne = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', sessionCookie.cookieString()) .expect(200); @@ -165,7 +165,7 @@ export default function ({ getService }) { expect(sessionCookieOne.value).to.not.equal(sessionCookie.value); const apiResponseTwo = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', sessionCookie.cookieString()) .expect(200); @@ -179,7 +179,7 @@ export default function ({ getService }) { it('should not extend cookie for system API calls', async () => { const systemAPIResponse = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('kbn-system-api', 'true') .set('Cookie', sessionCookie.cookieString()) @@ -190,7 +190,7 @@ export default function ({ getService }) { it('should fail and preserve session cookie if unsupported authentication schema is used', async () => { const apiResponse = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Authorization', 'Bearer AbCdEf') .set('Cookie', sessionCookie.cookieString()) @@ -200,7 +200,7 @@ export default function ({ getService }) { }); it('should clear cookie on logout and redirect to login', async ()=> { - const logoutResponse = await supertest.get('/api/security/v1/logout?next=%2Fabc%2Fxyz&msg=test') + const logoutResponse = await supertest.get('/api/security/logout?next=%2Fabc%2Fxyz&msg=test') .set('Cookie', sessionCookie.cookieString()) .expect(302); @@ -256,7 +256,7 @@ export default function ({ getService }) { }); it('should redirect to home page if cookie is not provided', async ()=> { - const logoutResponse = await supertest.get('/api/security/v1/logout') + const logoutResponse = await supertest.get('/api/security/logout') .expect(302); expect(logoutResponse.headers['set-cookie']).to.be(undefined); diff --git a/x-pack/test/api_integration/apis/security/change_password.ts b/x-pack/test/api_integration/apis/security/change_password.ts index 09751d4a3641a..3efb7eb2bb1dd 100644 --- a/x-pack/test/api_integration/apis/security/change_password.ts +++ b/x-pack/test/api_integration/apis/security/change_password.ts @@ -20,7 +20,7 @@ export default function({ getService }: FtrProviderContext) { await security.user.create(mockUserName, { password: mockUserPassword, roles: [] }); const loginResponse = await supertest - .post('/api/security/v1/login') + .post('/internal/security/login') .set('kbn-xsrf', 'xxx') .send({ username: mockUserName, password: mockUserPassword }) .expect(204); @@ -34,7 +34,7 @@ export default function({ getService }: FtrProviderContext) { const newPassword = `xxx-${mockUserPassword}-xxx`; await supertest - .post(`/api/security/v1/users/${mockUserName}/password`) + .post(`/internal/security/users/${mockUserName}/password`) .set('kbn-xsrf', 'xxx') .set('Cookie', sessionCookie.cookieString()) .send({ password: wrongPassword, newPassword }) @@ -42,21 +42,21 @@ export default function({ getService }: FtrProviderContext) { // Let's check that we can't login with wrong password, just in case. await supertest - .post('/api/security/v1/login') + .post('/internal/security/login') .set('kbn-xsrf', 'xxx') .send({ username: mockUserName, password: wrongPassword }) .expect(401); // Let's check that we can't login with the password we were supposed to set. await supertest - .post('/api/security/v1/login') + .post('/internal/security/login') .set('kbn-xsrf', 'xxx') .send({ username: mockUserName, password: newPassword }) .expect(401); // And can login with the current password. await supertest - .post('/api/security/v1/login') + .post('/internal/security/login') .set('kbn-xsrf', 'xxx') .send({ username: mockUserName, password: mockUserPassword }) .expect(204); @@ -66,7 +66,7 @@ export default function({ getService }: FtrProviderContext) { const newPassword = `xxx-${mockUserPassword}-xxx`; const passwordChangeResponse = await supertest - .post(`/api/security/v1/users/${mockUserName}/password`) + .post(`/internal/security/users/${mockUserName}/password`) .set('kbn-xsrf', 'xxx') .set('Cookie', sessionCookie.cookieString()) .send({ password: mockUserPassword, newPassword }) @@ -76,28 +76,28 @@ export default function({ getService }: FtrProviderContext) { // Let's check that previous cookie isn't valid anymore. await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', sessionCookie.cookieString()) .expect(401); // And that we can't login with the old password. await supertest - .post('/api/security/v1/login') + .post('/internal/security/login') .set('kbn-xsrf', 'xxx') .send({ username: mockUserName, password: mockUserPassword }) .expect(401); // But new cookie should be valid. await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', newSessionCookie.cookieString()) .expect(200); // And that we can login with new credentials. await supertest - .post('/api/security/v1/login') + .post('/internal/security/login') .set('kbn-xsrf', 'xxx') .send({ username: mockUserName, password: newPassword }) .expect(204); diff --git a/x-pack/test/api_integration/apis/security/index_fields.ts b/x-pack/test/api_integration/apis/security/index_fields.ts index 60c6e800c40b2..7adc589fbec3e 100644 --- a/x-pack/test/api_integration/apis/security/index_fields.ts +++ b/x-pack/test/api_integration/apis/security/index_fields.ts @@ -11,10 +11,10 @@ export default function({ getService }: FtrProviderContext) { const supertest = getService('supertest'); describe('Index Fields', () => { - describe('GET /api/security/v1/fields/{query}', () => { + describe('GET /internal/security/fields/{query}', () => { it('should return a list of available index mapping fields', async () => { await supertest - .get('/api/security/v1/fields/.kibana') + .get('/internal/security/fields/.kibana') .set('kbn-xsrf', 'xxx') .send() .expect(200) diff --git a/x-pack/test/api_integration/apis/security/session.ts b/x-pack/test/api_integration/apis/security/session.ts index 7c7883f58cb30..5d0935bb1ae2d 100644 --- a/x-pack/test/api_integration/apis/security/session.ts +++ b/x-pack/test/api_integration/apis/security/session.ts @@ -43,7 +43,7 @@ export default function({ getService }: FtrProviderContext) { beforeEach(async () => { await supertest - .post('/api/security/v1/login') + .post('/internal/security/login') .set('kbn-xsrf', 'xxx') .send({ username: validUsername, password: validPassword }) .expect(204) diff --git a/x-pack/test/api_integration/services/legacy_es.js b/x-pack/test/api_integration/services/legacy_es.js index 1518550407529..12a1576f78982 100644 --- a/x-pack/test/api_integration/services/legacy_es.js +++ b/x-pack/test/api_integration/services/legacy_es.js @@ -8,7 +8,7 @@ import { format as formatUrl } from 'url'; import * as legacyElasticsearch from 'elasticsearch'; -import shieldPlugin from '../../../legacy/server/lib/esjs_shield_plugin'; +import { elasticsearchClientPlugin } from '../../../plugins/security/server/elasticsearch_client_plugin'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { DEFAULT_API_VERSION } from '../../../../src/core/server/elasticsearch/elasticsearch_config'; @@ -19,6 +19,6 @@ export function LegacyEsProvider({ getService }) { apiVersion: DEFAULT_API_VERSION, host: formatUrl(config.get('servers.elasticsearch')), requestTimeout: config.get('timeouts.esRequestTimeout'), - plugins: [shieldPlugin], + plugins: [elasticsearchClientPlugin], }); } diff --git a/x-pack/test/kerberos_api_integration/apis/security/kerberos_login.ts b/x-pack/test/kerberos_api_integration/apis/security/kerberos_login.ts index 450f7b1a427dc..0346da334d2f2 100644 --- a/x-pack/test/kerberos_api_integration/apis/security/kerberos_login.ts +++ b/x-pack/test/kerberos_api_integration/apis/security/kerberos_login.ts @@ -47,7 +47,7 @@ export default function({ getService }: FtrProviderContext) { it('should reject API requests if client is not authenticated', async () => { await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .expect(401); }); @@ -55,7 +55,7 @@ export default function({ getService }: FtrProviderContext) { it('does not prevent basic login', async () => { const [username, password] = config.get('servers.elasticsearch.auth').split(':'); const response = await supertest - .post('/api/security/v1/login') + .post('/internal/security/login') .set('kbn-xsrf', 'xxx') .send({ username, password }) .expect(204); @@ -67,7 +67,7 @@ export default function({ getService }: FtrProviderContext) { checkCookieIsSet(cookie); const { body: user } = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', cookie.cookieString()) .expect(200); @@ -98,7 +98,7 @@ export default function({ getService }: FtrProviderContext) { describe('finishing SPNEGO', () => { it('should properly set cookie and authenticate user', async () => { const response = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('Authorization', `Negotiate ${spnegoToken}`) .expect(200); @@ -114,7 +114,7 @@ export default function({ getService }: FtrProviderContext) { checkCookieIsSet(sessionCookie); await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', sessionCookie.cookieString()) .expect(200, { @@ -134,7 +134,7 @@ export default function({ getService }: FtrProviderContext) { it('should re-initiate SPNEGO handshake if token is rejected with 401', async () => { const spnegoResponse = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('Authorization', `Negotiate ${Buffer.from('Hello').toString('base64')}`) .expect(401); expect(spnegoResponse.headers['set-cookie']).to.be(undefined); @@ -143,7 +143,7 @@ export default function({ getService }: FtrProviderContext) { it('should fail if SPNEGO token is rejected because of unknown reason', async () => { const spnegoResponse = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('Authorization', 'Negotiate (:I am malformed:)') .expect(500); expect(spnegoResponse.headers['set-cookie']).to.be(undefined); @@ -156,7 +156,7 @@ export default function({ getService }: FtrProviderContext) { beforeEach(async () => { const response = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('Authorization', `Negotiate ${spnegoToken}`) .expect(200); @@ -169,7 +169,7 @@ export default function({ getService }: FtrProviderContext) { it('should extend cookie on every successful non-system API call', async () => { const apiResponseOne = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', sessionCookie.cookieString()) .expect(200); @@ -181,7 +181,7 @@ export default function({ getService }: FtrProviderContext) { expect(sessionCookieOne.value).to.not.equal(sessionCookie.value); const apiResponseTwo = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', sessionCookie.cookieString()) .expect(200); @@ -195,7 +195,7 @@ export default function({ getService }: FtrProviderContext) { it('should not extend cookie for system API calls', async () => { const systemAPIResponse = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('kbn-system-api', 'true') .set('Cookie', sessionCookie.cookieString()) @@ -206,7 +206,7 @@ export default function({ getService }: FtrProviderContext) { it('should fail and preserve session cookie if unsupported authentication schema is used', async () => { const apiResponse = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Authorization', 'Basic a3JiNTprcmI1') .set('Cookie', sessionCookie.cookieString()) @@ -220,7 +220,7 @@ export default function({ getService }: FtrProviderContext) { it('should redirect to `logged_out` page after successful logout', async () => { // First authenticate user to retrieve session cookie. const response = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('Authorization', `Negotiate ${spnegoToken}`) .expect(200); @@ -232,7 +232,7 @@ export default function({ getService }: FtrProviderContext) { // And then log user out. const logoutResponse = await supertest - .get('/api/security/v1/logout') + .get('/api/security/logout') .set('Cookie', sessionCookie.cookieString()) .expect(302); @@ -245,7 +245,7 @@ export default function({ getService }: FtrProviderContext) { // Token that was stored in the previous cookie should be invalidated as well and old // session cookie should not allow API access. const apiResponse = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', sessionCookie.cookieString()) .expect(401); @@ -259,7 +259,7 @@ export default function({ getService }: FtrProviderContext) { }); it('should redirect to home page if session cookie is not provided', async () => { - const logoutResponse = await supertest.get('/api/security/v1/logout').expect(302); + const logoutResponse = await supertest.get('/api/security/logout').expect(302); expect(logoutResponse.headers['set-cookie']).to.be(undefined); expect(logoutResponse.headers.location).to.be('/'); @@ -271,7 +271,7 @@ export default function({ getService }: FtrProviderContext) { beforeEach(async () => { const response = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('Authorization', `Negotiate ${spnegoToken}`) .expect(200); @@ -292,7 +292,7 @@ export default function({ getService }: FtrProviderContext) { // This api call should succeed and automatically refresh token. Returned cookie will contain // the new access and refresh token pair. const apiResponse = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', sessionCookie.cookieString()) .expect(200); @@ -305,7 +305,7 @@ export default function({ getService }: FtrProviderContext) { // The first new cookie with fresh pair of access and refresh tokens should work. await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', refreshedCookie.cookieString()) .expect(200); @@ -335,7 +335,7 @@ export default function({ getService }: FtrProviderContext) { // The first new cookie with fresh pair of access and refresh tokens should work. await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', refreshedCookie.cookieString()) .expect(200); @@ -349,7 +349,7 @@ export default function({ getService }: FtrProviderContext) { beforeEach(async () => { const response = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('Authorization', `Negotiate ${spnegoToken}`) .expect(200); @@ -374,7 +374,7 @@ export default function({ getService }: FtrProviderContext) { it('AJAX call should initiate SPNEGO and clear existing cookie', async function() { const apiResponse = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', sessionCookie.cookieString()) .expect(401); diff --git a/x-pack/test/oidc_api_integration/apis/authorization_code_flow/oidc_auth.js b/x-pack/test/oidc_api_integration/apis/authorization_code_flow/oidc_auth.js index 80ef6bd6df4ff..95958d12a42d7 100644 --- a/x-pack/test/oidc_api_integration/apis/authorization_code_flow/oidc_auth.js +++ b/x-pack/test/oidc_api_integration/apis/authorization_code_flow/oidc_auth.js @@ -16,7 +16,7 @@ export default function ({ getService }) { describe('OpenID Connect authentication', () => { it('should reject API requests if client is not authenticated', async () => { await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .expect(401); }); @@ -46,7 +46,8 @@ export default function ({ getService }) { }); it('should properly set cookie, return all parameters and redirect user for Third Party initiated', async () => { - const handshakeResponse = await supertest.get('/api/security/v1/oidc?iss=https://test-op.elastic.co') + const handshakeResponse = await supertest.post('/api/security/oidc/initiate_login') + .send({ iss: 'https://test-op.elastic.co' }) .expect(302); const cookies = handshakeResponse.headers['set-cookie']; @@ -74,7 +75,7 @@ export default function ({ getService }) { const handshakeCookie = request.cookie(handshakeResponse.headers['set-cookie'][0]); await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', handshakeCookie.cookieString()) .expect(401); @@ -108,20 +109,20 @@ export default function ({ getService }) { }); it('should fail if OpenID Connect response is not complemented with handshake cookie', async () => { - await supertest.get(`/api/security/v1/oidc?code=thisisthecode&state=${stateAndNonce.state}`) + await supertest.get(`/api/security/oidc?code=thisisthecode&state=${stateAndNonce.state}`) .set('kbn-xsrf', 'xxx') .expect(401); }); it('should fail if state is not matching', async () => { - await supertest.get(`/api/security/v1/oidc?code=thisisthecode&state=someothervalue`) + await supertest.get(`/api/security/oidc?code=thisisthecode&state=someothervalue`) .set('kbn-xsrf', 'xxx') .set('Cookie', handshakeCookie.cookieString()) .expect(401); }); it('should succeed if both the OpenID Connect response and the cookie are provided', async () => { - const oidcAuthenticationResponse = await supertest.get(`/api/security/v1/oidc?code=code1&state=${stateAndNonce.state}`) + const oidcAuthenticationResponse = await supertest.get(`/api/security/oidc?code=code1&state=${stateAndNonce.state}`) .set('kbn-xsrf', 'xxx') .set('Cookie', handshakeCookie.cookieString()) .expect(302); @@ -139,7 +140,7 @@ export default function ({ getService }) { expect(sessionCookie.httpOnly).to.be(true); const apiResponse = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', sessionCookie.cookieString()) .expect(200); @@ -160,7 +161,7 @@ export default function ({ getService }) { describe('Complete third party initiated authentication', () => { it('should authenticate a user when a third party initiates the authentication', async () => { - const handshakeResponse = await supertest.get('/api/security/v1/oidc?iss=https://test-op.elastic.co') + const handshakeResponse = await supertest.get('/api/security/oidc/initiate_login?iss=https://test-op.elastic.co') .expect(302); const handshakeCookie = request.cookie(handshakeResponse.headers['set-cookie'][0]); const stateAndNonce = getStateAndNonce(handshakeResponse.headers.location); @@ -172,7 +173,7 @@ export default function ({ getService }) { .send({ nonce: stateAndNonce.nonce }) .expect(200); - const oidcAuthenticationResponse = await supertest.get(`/api/security/v1/oidc?code=code2&state=${stateAndNonce.state}`) + const oidcAuthenticationResponse = await supertest.get(`/api/security/oidc?code=code2&state=${stateAndNonce.state}`) .set('kbn-xsrf', 'xxx') .set('Cookie', handshakeCookie.cookieString()) .expect(302); @@ -186,7 +187,7 @@ export default function ({ getService }) { expect(sessionCookie.httpOnly).to.be(true); const apiResponse = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', sessionCookie.cookieString()) .expect(200); @@ -222,7 +223,7 @@ export default function ({ getService }) { .send({ nonce: stateAndNonce.nonce }) .expect(200); - const oidcAuthenticationResponse = await supertest.get(`/api/security/v1/oidc?code=code1&state=${stateAndNonce.state}`) + const oidcAuthenticationResponse = await supertest.get(`/api/security/oidc?code=code1&state=${stateAndNonce.state}`) .set('kbn-xsrf', 'xxx') .set('Cookie', sessionCookie.cookieString()) .expect(302); @@ -232,7 +233,7 @@ export default function ({ getService }) { it('should extend cookie on every successful non-system API call', async () => { const apiResponseOne = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', sessionCookie.cookieString()) .expect(200); @@ -244,7 +245,7 @@ export default function ({ getService }) { expect(sessionCookieOne.value).to.not.equal(sessionCookie.value); const apiResponseTwo = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', sessionCookie.cookieString()) .expect(200); @@ -258,7 +259,7 @@ export default function ({ getService }) { it('should not extend cookie for system API calls', async () => { const systemAPIResponse = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('kbn-system-api', 'true') .set('Cookie', sessionCookie.cookieString()) @@ -269,7 +270,7 @@ export default function ({ getService }) { it('should fail and preserve session cookie if unsupported authentication schema is used', async () => { const apiResponse = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Authorization', 'Basic AbCdEf') .set('Cookie', sessionCookie.cookieString()) @@ -295,7 +296,7 @@ export default function ({ getService }) { .send({ nonce: stateAndNonce.nonce }) .expect(200); - const oidcAuthenticationResponse = await supertest.get(`/api/security/v1/oidc?code=code1&state=${stateAndNonce.state}`) + const oidcAuthenticationResponse = await supertest.get(`/api/security/oidc?code=code1&state=${stateAndNonce.state}`) .set('kbn-xsrf', 'xxx') .set('Cookie', handshakeCookie.cookieString()) .expect(302); @@ -307,7 +308,7 @@ export default function ({ getService }) { }); it('should redirect to home page if session cookie is not provided', async () => { - const logoutResponse = await supertest.get('/api/security/v1/logout') + const logoutResponse = await supertest.get('/api/security/logout') .expect(302); expect(logoutResponse.headers['set-cookie']).to.be(undefined); @@ -315,7 +316,7 @@ export default function ({ getService }) { }); it('should redirect to the OPs endsession endpoint to complete logout', async () => { - const logoutResponse = await supertest.get('/api/security/v1/logout') + const logoutResponse = await supertest.get('/api/security/logout') .set('Cookie', sessionCookie.cookieString()) .expect(302); @@ -336,7 +337,7 @@ export default function ({ getService }) { // Tokens that were stored in the previous cookie should be invalidated as well and old // session cookie should not allow API access. const apiResponse = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', sessionCookie.cookieString()) .expect(400); @@ -349,7 +350,7 @@ export default function ({ getService }) { }); it('should reject AJAX requests', async () => { - const ajaxResponse = await supertest.get('/api/security/v1/logout') + const ajaxResponse = await supertest.get('/api/security/logout') .set('kbn-xsrf', 'xxx') .set('Cookie', sessionCookie.cookieString()) .expect(400); @@ -379,7 +380,7 @@ export default function ({ getService }) { .send({ nonce: stateAndNonce.nonce }) .expect(200); - const oidcAuthenticationResponse = await supertest.get(`/api/security/v1/oidc?code=code1&state=${stateAndNonce.state}`) + const oidcAuthenticationResponse = await supertest.get(`/api/security/oidc?code=code1&state=${stateAndNonce.state}`) .set('kbn-xsrf', 'xxx') .set('Cookie', handshakeCookie.cookieString()) .expect(302); @@ -408,7 +409,7 @@ export default function ({ getService }) { // This api call should succeed and automatically refresh token. Returned cookie will contain // the new access and refresh token pair. const firstResponse = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', sessionCookie.cookieString()) .expect(200); @@ -422,7 +423,7 @@ export default function ({ getService }) { // Request with old cookie should reuse the same refresh token if within 60 seconds. // Returned cookie will contain the same new access and refresh token pairs as the first request const secondResponse = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', sessionCookie.cookieString()) .expect(200); @@ -437,14 +438,14 @@ export default function ({ getService }) { // The first new cookie with fresh pair of access and refresh tokens should work. await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', firstNewCookie.cookieString()) .expect(200); // The second new cookie with fresh pair of access and refresh tokens should work. await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', secondNewCookie.cookieString()) .expect(200); @@ -467,7 +468,7 @@ export default function ({ getService }) { .send({ nonce: stateAndNonce.nonce }) .expect(200); - const oidcAuthenticationResponse = await supertest.get(`/api/security/v1/oidc?code=code1&state=${stateAndNonce.state}`) + const oidcAuthenticationResponse = await supertest.get(`/api/security/oidc?code=code1&state=${stateAndNonce.state}`) .set('kbn-xsrf', 'xxx') .set('Cookie', handshakeCookie.cookieString()) .expect(302); diff --git a/x-pack/test/oidc_api_integration/apis/implicit_flow/oidc_auth.ts b/x-pack/test/oidc_api_integration/apis/implicit_flow/oidc_auth.ts index 23cbb312b092a..0e07f01776713 100644 --- a/x-pack/test/oidc_api_integration/apis/implicit_flow/oidc_auth.ts +++ b/x-pack/test/oidc_api_integration/apis/implicit_flow/oidc_auth.ts @@ -31,7 +31,7 @@ export default function({ getService }: FtrProviderContext) { }); it('should return an HTML page that will parse URL fragment', async () => { - const response = await supertest.get('/api/security/v1/oidc/implicit').expect(200); + const response = await supertest.get('/api/security/oidc/implicit').expect(200); const dom = new JSDOM(response.text, { url: formatURL({ ...config.get('servers.kibana'), auth: false }), runScripts: 'dangerously', @@ -44,7 +44,7 @@ export default function({ getService }: FtrProviderContext) { Object.defineProperty(window, 'location', { value: { href: - 'https://kibana.com/api/security/v1/oidc/implicit#token=some_token&access_token=some_access_token', + 'https://kibana.com/api/security/oidc/implicit#token=some_token&access_token=some_access_token', replace(newLocation: string) { this.href = newLocation; resolve(); @@ -66,17 +66,17 @@ export default function({ getService }: FtrProviderContext) { // Check that script that forwards URL fragment worked correctly. expect(dom.window.location.href).to.be( - '/api/security/v1/oidc?authenticationResponseURI=https%3A%2F%2Fkibana.com%2Fapi%2Fsecurity%2Fv1%2Foidc%2Fimplicit%23token%3Dsome_token%26access_token%3Dsome_access_token' + '/api/security/oidc?authenticationResponseURI=https%3A%2F%2Fkibana.com%2Fapi%2Fsecurity%2Foidc%2Fimplicit%23token%3Dsome_token%26access_token%3Dsome_access_token' ); }); it('should fail if OpenID Connect response is not complemented with handshake cookie', async () => { const { idToken, accessToken } = createTokens('1', stateAndNonce.nonce); - const authenticationResponse = `https://kibana.com/api/security/v1/oidc/implicit#id_token=${idToken}&state=${stateAndNonce.state}&token_type=bearer&access_token=${accessToken}`; + const authenticationResponse = `https://kibana.com/api/security/oidc/implicit#id_token=${idToken}&state=${stateAndNonce.state}&token_type=bearer&access_token=${accessToken}`; await supertest .get( - `/api/security/v1/oidc?authenticationResponseURI=${encodeURIComponent( + `/api/security/oidc?authenticationResponseURI=${encodeURIComponent( authenticationResponse )}` ) @@ -86,11 +86,11 @@ export default function({ getService }: FtrProviderContext) { it('should fail if state is not matching', async () => { const { idToken, accessToken } = createTokens('1', stateAndNonce.nonce); - const authenticationResponse = `https://kibana.com/api/security/v1/oidc/implicit#id_token=${idToken}&state=$someothervalue&token_type=bearer&access_token=${accessToken}`; + const authenticationResponse = `https://kibana.com/api/security/oidc/implicit#id_token=${idToken}&state=$someothervalue&token_type=bearer&access_token=${accessToken}`; await supertest .get( - `/api/security/v1/oidc?authenticationResponseURI=${encodeURIComponent( + `/api/security/oidc?authenticationResponseURI=${encodeURIComponent( authenticationResponse )}` ) @@ -102,11 +102,11 @@ export default function({ getService }: FtrProviderContext) { // FLAKY: https://github.com/elastic/kibana/issues/43938 it.skip('should succeed if both the OpenID Connect response and the cookie are provided', async () => { const { idToken, accessToken } = createTokens('1', stateAndNonce.nonce); - const authenticationResponse = `https://kibana.com/api/security/v1/oidc/implicit#id_token=${idToken}&state=${stateAndNonce.state}&token_type=bearer&access_token=${accessToken}`; + const authenticationResponse = `https://kibana.com/api/security/oidc/implicit#id_token=${idToken}&state=${stateAndNonce.state}&token_type=bearer&access_token=${accessToken}`; const oidcAuthenticationResponse = await supertest .get( - `/api/security/v1/oidc?authenticationResponseURI=${encodeURIComponent( + `/api/security/oidc?authenticationResponseURI=${encodeURIComponent( authenticationResponse )}` ) @@ -129,7 +129,7 @@ export default function({ getService }: FtrProviderContext) { expect(sessionCookie.httpOnly).to.be(true); const apiResponse = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', sessionCookie.cookieString()) .expect(200); diff --git a/x-pack/test/oidc_api_integration/config.ts b/x-pack/test/oidc_api_integration/config.ts index f40db4ccbba0a..184ccbcdfa691 100644 --- a/x-pack/test/oidc_api_integration/config.ts +++ b/x-pack/test/oidc_api_integration/config.ts @@ -32,7 +32,7 @@ export default async function({ readConfigFile }: FtrConfigProviderContext) { `xpack.security.authc.realms.oidc.oidc1.rp.client_id=0oa8sqpov3TxMWJOt356`, `xpack.security.authc.realms.oidc.oidc1.rp.client_secret=0oa8sqpov3TxMWJOt356`, `xpack.security.authc.realms.oidc.oidc1.rp.response_type=code`, - `xpack.security.authc.realms.oidc.oidc1.rp.redirect_uri=http://localhost:${kibanaPort}/api/security/v1/oidc`, + `xpack.security.authc.realms.oidc.oidc1.rp.redirect_uri=http://localhost:${kibanaPort}/api/security/oidc`, `xpack.security.authc.realms.oidc.oidc1.op.authorization_endpoint=https://test-op.elastic.co/oauth2/v1/authorize`, `xpack.security.authc.realms.oidc.oidc1.op.endsession_endpoint=https://test-op.elastic.co/oauth2/v1/endsession`, `xpack.security.authc.realms.oidc.oidc1.op.token_endpoint=http://localhost:${kibanaPort}/api/oidc_provider/token_endpoint`, @@ -52,7 +52,7 @@ export default async function({ readConfigFile }: FtrConfigProviderContext) { '--xpack.security.authc.oidc.realm="oidc1"', '--server.xsrf.whitelist', JSON.stringify([ - '/api/security/v1/oidc', + '/api/security/oidc/initiate_login', '/api/oidc_provider/token_endpoint', '/api/oidc_provider/userinfo_endpoint', ]), diff --git a/x-pack/test/pki_api_integration/apis/security/pki_auth.ts b/x-pack/test/pki_api_integration/apis/security/pki_auth.ts index afb27168d6d5c..4eee900e68bec 100644 --- a/x-pack/test/pki_api_integration/apis/security/pki_auth.ts +++ b/x-pack/test/pki_api_integration/apis/security/pki_auth.ts @@ -57,7 +57,7 @@ export default function({ getService }: FtrProviderContext) { it('should reject API requests that use untrusted certificate', async () => { await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .ca(CA_CERT) .pfx(UNTRUSTED_CLIENT_CERT) .set('kbn-xsrf', 'xxx') @@ -67,7 +67,7 @@ export default function({ getService }: FtrProviderContext) { it('does not prevent basic login', async () => { const [username, password] = config.get('servers.elasticsearch.auth').split(':'); const response = await supertest - .post('/api/security/v1/login') + .post('/internal/security/login') .ca(CA_CERT) .pfx(UNTRUSTED_CLIENT_CERT) .set('kbn-xsrf', 'xxx') @@ -81,7 +81,7 @@ export default function({ getService }: FtrProviderContext) { checkCookieIsSet(cookie); const { body: user } = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .ca(CA_CERT) .pfx(UNTRUSTED_CLIENT_CERT) .set('kbn-xsrf', 'xxx') @@ -94,7 +94,7 @@ export default function({ getService }: FtrProviderContext) { it('should properly set cookie and authenticate user', async () => { const response = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .ca(CA_CERT) .pfx(FIRST_CLIENT_CERT) .expect(200); @@ -122,7 +122,7 @@ export default function({ getService }: FtrProviderContext) { // Cookie should be accepted. await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .ca(CA_CERT) .pfx(FIRST_CLIENT_CERT) .set('Cookie', sessionCookie.cookieString()) @@ -131,7 +131,7 @@ export default function({ getService }: FtrProviderContext) { it('should update session if new certificate is provided', async () => { let response = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .ca(CA_CERT) .pfx(FIRST_CLIENT_CERT) .expect(200); @@ -143,7 +143,7 @@ export default function({ getService }: FtrProviderContext) { checkCookieIsSet(sessionCookie); response = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .ca(CA_CERT) .pfx(SECOND_CLIENT_CERT) .set('Cookie', sessionCookie.cookieString()) @@ -167,7 +167,7 @@ export default function({ getService }: FtrProviderContext) { it('should reject valid cookie if used with untrusted certificate', async () => { const response = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .ca(CA_CERT) .pfx(FIRST_CLIENT_CERT) .expect(200); @@ -179,7 +179,7 @@ export default function({ getService }: FtrProviderContext) { checkCookieIsSet(sessionCookie); await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .ca(CA_CERT) .pfx(UNTRUSTED_CLIENT_CERT) .set('Cookie', sessionCookie.cookieString()) @@ -191,7 +191,7 @@ export default function({ getService }: FtrProviderContext) { beforeEach(async () => { const response = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .ca(CA_CERT) .pfx(FIRST_CLIENT_CERT) .expect(200); @@ -205,7 +205,7 @@ export default function({ getService }: FtrProviderContext) { it('should extend cookie on every successful non-system API call', async () => { const apiResponseOne = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .ca(CA_CERT) .pfx(FIRST_CLIENT_CERT) .set('kbn-xsrf', 'xxx') @@ -219,7 +219,7 @@ export default function({ getService }: FtrProviderContext) { expect(sessionCookieOne.value).to.not.equal(sessionCookie.value); const apiResponseTwo = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .ca(CA_CERT) .pfx(FIRST_CLIENT_CERT) .set('kbn-xsrf', 'xxx') @@ -235,7 +235,7 @@ export default function({ getService }: FtrProviderContext) { it('should not extend cookie for system API calls', async () => { const systemAPIResponse = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .ca(CA_CERT) .pfx(FIRST_CLIENT_CERT) .set('kbn-xsrf', 'xxx') @@ -248,7 +248,7 @@ export default function({ getService }: FtrProviderContext) { it('should fail and preserve session cookie if unsupported authentication schema is used', async () => { const apiResponse = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .ca(CA_CERT) .pfx(FIRST_CLIENT_CERT) .set('kbn-xsrf', 'xxx') @@ -264,7 +264,7 @@ export default function({ getService }: FtrProviderContext) { it('should redirect to `logged_out` page after successful logout', async () => { // First authenticate user to retrieve session cookie. const response = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .ca(CA_CERT) .pfx(FIRST_CLIENT_CERT) .expect(200); @@ -277,7 +277,7 @@ export default function({ getService }: FtrProviderContext) { // And then log user out. const logoutResponse = await supertest - .get('/api/security/v1/logout') + .get('/api/security/logout') .ca(CA_CERT) .pfx(FIRST_CLIENT_CERT) .set('Cookie', sessionCookie.cookieString()) @@ -292,7 +292,7 @@ export default function({ getService }: FtrProviderContext) { it('should redirect to home page if session cookie is not provided', async () => { const logoutResponse = await supertest - .get('/api/security/v1/logout') + .get('/api/security/logout') .ca(CA_CERT) .pfx(FIRST_CLIENT_CERT) .expect(302); @@ -307,7 +307,7 @@ export default function({ getService }: FtrProviderContext) { beforeEach(async () => { const response = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .ca(CA_CERT) .pfx(FIRST_CLIENT_CERT) .expect(200); @@ -329,7 +329,7 @@ export default function({ getService }: FtrProviderContext) { // This api call should succeed and automatically refresh token. Returned cookie will contain // the new access token. const apiResponse = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .ca(CA_CERT) .pfx(FIRST_CLIENT_CERT) .set('kbn-xsrf', 'xxx') diff --git a/x-pack/test/saml_api_integration/apis/security/saml_login.ts b/x-pack/test/saml_api_integration/apis/security/saml_login.ts index 3815788aa746e..0436d59906ea8 100644 --- a/x-pack/test/saml_api_integration/apis/security/saml_login.ts +++ b/x-pack/test/saml_api_integration/apis/security/saml_login.ts @@ -42,7 +42,7 @@ export default function({ getService }: FtrProviderContext) { expect(sessionCookie.httpOnly).to.be(true); const apiResponse = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', sessionCookie.cookieString()) .expect(200); @@ -64,7 +64,7 @@ export default function({ getService }: FtrProviderContext) { describe('SAML authentication', () => { it('should reject API requests if client is not authenticated', async () => { await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .expect(401); }); @@ -72,7 +72,7 @@ export default function({ getService }: FtrProviderContext) { it('does not prevent basic login', async () => { const [username, password] = config.get('servers.elasticsearch.auth').split(':'); const response = await supertest - .post('/api/security/v1/login') + .post('/internal/security/login') .set('kbn-xsrf', 'xxx') .send({ username, password }) .expect(204); @@ -81,7 +81,7 @@ export default function({ getService }: FtrProviderContext) { expect(cookies).to.have.length(1); const { body: user } = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', request.cookie(cookies[0])!.cookieString()) .expect(200); @@ -192,7 +192,7 @@ export default function({ getService }: FtrProviderContext) { const handshakeCookie = request.cookie(handshakeResponse.headers['set-cookie'][0])!; await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', handshakeCookie.cookieString()) .expect(401); @@ -300,7 +300,7 @@ export default function({ getService }: FtrProviderContext) { it('should extend cookie on every successful non-system API call', async () => { const apiResponseOne = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', sessionCookie.cookieString()) .expect(200); @@ -312,7 +312,7 @@ export default function({ getService }: FtrProviderContext) { expect(sessionCookieOne.value).to.not.equal(sessionCookie.value); const apiResponseTwo = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', sessionCookie.cookieString()) .expect(200); @@ -326,7 +326,7 @@ export default function({ getService }: FtrProviderContext) { it('should not extend cookie for system API calls', async () => { const systemAPIResponse = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('kbn-system-api', 'true') .set('Cookie', sessionCookie.cookieString()) @@ -337,7 +337,7 @@ export default function({ getService }: FtrProviderContext) { it('should fail and preserve session cookie if unsupported authentication schema is used', async () => { const apiResponse = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Authorization', 'Basic AbCdEf') .set('Cookie', sessionCookie.cookieString()) @@ -383,7 +383,7 @@ export default function({ getService }: FtrProviderContext) { it('should redirect to IdP with SAML request to complete logout', async () => { const logoutResponse = await supertest - .get('/api/security/v1/logout') + .get('/api/security/logout') .set('Cookie', sessionCookie.cookieString()) .expect(302); @@ -404,7 +404,7 @@ export default function({ getService }: FtrProviderContext) { // Tokens that were stored in the previous cookie should be invalidated as well and old // session cookie should not allow API access. const apiResponse = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', sessionCookie.cookieString()) .expect(400); @@ -417,7 +417,7 @@ export default function({ getService }: FtrProviderContext) { }); it('should redirect to home page if session cookie is not provided', async () => { - const logoutResponse = await supertest.get('/api/security/v1/logout').expect(302); + const logoutResponse = await supertest.get('/api/security/logout').expect(302); expect(logoutResponse.headers['set-cookie']).to.be(undefined); expect(logoutResponse.headers.location).to.be('/'); @@ -425,7 +425,7 @@ export default function({ getService }: FtrProviderContext) { it('should reject AJAX requests', async () => { const ajaxResponse = await supertest - .get('/api/security/v1/logout') + .get('/api/security/logout') .set('kbn-xsrf', 'xxx') .set('Cookie', sessionCookie.cookieString()) .expect(400); @@ -441,7 +441,7 @@ export default function({ getService }: FtrProviderContext) { it('should invalidate access token on IdP initiated logout', async () => { const logoutRequest = await createLogoutRequest({ sessionIndex: idpSessionIndex }); const logoutResponse = await supertest - .get(`/api/security/v1/logout?${querystring.stringify(logoutRequest)}`) + .get(`/api/security/logout?${querystring.stringify(logoutRequest)}`) .set('Cookie', sessionCookie.cookieString()) .expect(302); @@ -462,7 +462,7 @@ export default function({ getService }: FtrProviderContext) { // Tokens that were stored in the previous cookie should be invalidated as well and old session // cookie should not allow API access. const apiResponse = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', sessionCookie.cookieString()) .expect(400); @@ -477,7 +477,7 @@ export default function({ getService }: FtrProviderContext) { it('should invalidate access token on IdP initiated logout even if there is no Kibana session', async () => { const logoutRequest = await createLogoutRequest({ sessionIndex: idpSessionIndex }); const logoutResponse = await supertest - .get(`/api/security/v1/logout?${querystring.stringify(logoutRequest)}`) + .get(`/api/security/logout?${querystring.stringify(logoutRequest)}`) .expect(302); expect(logoutResponse.headers['set-cookie']).to.be(undefined); @@ -490,7 +490,7 @@ export default function({ getService }: FtrProviderContext) { // IdP session id (encoded in SAML LogoutRequest) even if Kibana doesn't provide them and session // cookie with these tokens should not allow API access. const apiResponse = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', sessionCookie.cookieString()) .expect(400); @@ -548,7 +548,7 @@ export default function({ getService }: FtrProviderContext) { // This api call should succeed and automatically refresh token. Returned cookie will contain // the new access and refresh token pair. const firstResponse = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', sessionCookie.cookieString()) .expect(200); @@ -562,7 +562,7 @@ export default function({ getService }: FtrProviderContext) { // Request with old cookie should reuse the same refresh token if within 60 seconds. // Returned cookie will contain the same new access and refresh token pairs as the first request const secondResponse = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', sessionCookie.cookieString()) .expect(200); @@ -577,14 +577,14 @@ export default function({ getService }: FtrProviderContext) { // The first new cookie with fresh pair of access and refresh tokens should work. await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', firstNewCookie.cookieString()) .expect(200); // The second new cookie with fresh pair of access and refresh tokens should work. await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', secondNewCookie.cookieString()) .expect(200); @@ -701,7 +701,7 @@ export default function({ getService }: FtrProviderContext) { // Tokens from old cookie are invalidated. const rejectedResponse = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', existingSessionCookie.cookieString()) .expect(400); @@ -712,7 +712,7 @@ export default function({ getService }: FtrProviderContext) { // Only tokens from new session are valid. const acceptedResponse = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', newSessionCookie.cookieString()) .expect(200); @@ -737,7 +737,7 @@ export default function({ getService }: FtrProviderContext) { // Tokens from old cookie are invalidated. const rejectedResponse = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', existingSessionCookie.cookieString()) .expect(400); @@ -748,7 +748,7 @@ export default function({ getService }: FtrProviderContext) { // Only tokens from new session are valid. const acceptedResponse = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', newSessionCookie.cookieString()) .expect(200); diff --git a/x-pack/test/saved_object_api_integration/common/services/legacy_es.js b/x-pack/test/saved_object_api_integration/common/services/legacy_es.js index 94aa6025aa699..9267fa312ed06 100644 --- a/x-pack/test/saved_object_api_integration/common/services/legacy_es.js +++ b/x-pack/test/saved_object_api_integration/common/services/legacy_es.js @@ -8,7 +8,7 @@ import { format as formatUrl } from 'url'; import * as legacyElasticsearch from 'elasticsearch'; -import shieldPlugin from '../../../../legacy/server/lib/esjs_shield_plugin'; +import { elasticsearchClientPlugin } from '../../../../plugins/security/server/elasticsearch_client_plugin'; export function LegacyEsProvider({ getService }) { const config = getService('config'); @@ -16,6 +16,6 @@ export function LegacyEsProvider({ getService }) { return new legacyElasticsearch.Client({ host: formatUrl(config.get('servers.elasticsearch')), requestTimeout: config.get('timeouts.esRequestTimeout'), - plugins: [shieldPlugin], + plugins: [elasticsearchClientPlugin], }); } diff --git a/x-pack/test/spaces_api_integration/common/services/legacy_es.js b/x-pack/test/spaces_api_integration/common/services/legacy_es.js index 5e8137f0d11b5..5862fe877ba5c 100644 --- a/x-pack/test/spaces_api_integration/common/services/legacy_es.js +++ b/x-pack/test/spaces_api_integration/common/services/legacy_es.js @@ -8,7 +8,7 @@ import { format as formatUrl } from 'url'; import * as legacyElasticsearch from 'elasticsearch'; -import shieldPlugin from '../../../../legacy/server/lib/esjs_shield_plugin'; +import { elasticsearchClientPlugin } from '../../../../plugins/security/server/elasticsearch_client_plugin'; export function LegacyEsProvider({ getService }) { const config = getService('config'); @@ -16,6 +16,6 @@ export function LegacyEsProvider({ getService }) { return new legacyElasticsearch.Client({ host: formatUrl(config.get('servers.elasticsearch')), requestTimeout: config.get('timeouts.esRequestTimeout'), - plugins: [shieldPlugin] + plugins: [elasticsearchClientPlugin] }); } diff --git a/x-pack/test/token_api_integration/auth/header.js b/x-pack/test/token_api_integration/auth/header.js index 4b27fd5db3166..1c88f28a65541 100644 --- a/x-pack/test/token_api_integration/auth/header.js +++ b/x-pack/test/token_api_integration/auth/header.js @@ -25,7 +25,7 @@ export default function ({ getService }) { const token = await createToken(); await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'true') .set('authorization', `Bearer ${token}`) .expect(200); @@ -36,14 +36,14 @@ export default function ({ getService }) { // try it once await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'true') .set('authorization', `Bearer ${token}`) .expect(200); // try it again to verity it isn't invalidated after a single request await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'true') .set('authorization', `Bearer ${token}`) .expect(200); @@ -51,7 +51,7 @@ export default function ({ getService }) { it('rejects invalid access token via authorization Bearer header', async () => { await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'true') .set('authorization', 'Bearer notreal') .expect(401); @@ -67,7 +67,7 @@ export default function ({ getService }) { await new Promise(resolve => setTimeout(() => resolve(), 20000)); await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'true') .set('authorization', `Bearer ${token}`) .expect(401); diff --git a/x-pack/test/token_api_integration/auth/login.js b/x-pack/test/token_api_integration/auth/login.js index 2e6a2e2f81e4f..aba7e3852aa1f 100644 --- a/x-pack/test/token_api_integration/auth/login.js +++ b/x-pack/test/token_api_integration/auth/login.js @@ -17,7 +17,7 @@ export default function ({ getService }) { describe('login', () => { it('accepts valid login credentials as 204 status', async () => { await supertest - .post('/api/security/v1/login') + .post('/internal/security/login') .set('kbn-xsrf', 'true') .send({ username: 'elastic', password: 'changeme' }) .expect(204); @@ -25,7 +25,7 @@ export default function ({ getService }) { it('sets HttpOnly cookie with valid login', async () => { const response = await supertest - .post('/api/security/v1/login') + .post('/internal/security/login') .set('kbn-xsrf', 'true') .send({ username: 'elastic', password: 'changeme' }) .expect(204); @@ -42,7 +42,7 @@ export default function ({ getService }) { it('rejects without kbn-xsrf header as 400 status even if credentials are valid', async () => { const response = await supertest - .post('/api/security/v1/login') + .post('/internal/security/login') .send({ username: 'elastic', password: 'changeme' }) .expect(400); @@ -53,7 +53,7 @@ export default function ({ getService }) { it('rejects without credentials as 400 status', async () => { const response = await supertest - .post('/api/security/v1/login') + .post('/internal/security/login') .set('kbn-xsrf', 'true') .expect(400); @@ -64,7 +64,7 @@ export default function ({ getService }) { it('rejects without password as 400 status', async () => { const response = await supertest - .post('/api/security/v1/login') + .post('/internal/security/login') .set('kbn-xsrf', 'true') .send({ username: 'elastic' }) .expect(400); @@ -76,7 +76,7 @@ export default function ({ getService }) { it('rejects without username as 400 status', async () => { const response = await supertest - .post('/api/security/v1/login') + .post('/internal/security/login') .set('kbn-xsrf', 'true') .send({ password: 'changme' }) .expect(400); @@ -88,7 +88,7 @@ export default function ({ getService }) { it('rejects invalid credentials as 401 status', async () => { const response = await supertest - .post('/api/security/v1/login') + .post('/internal/security/login') .set('kbn-xsrf', 'true') .send({ username: 'elastic', password: 'notvalidpassword' }) .expect(401); diff --git a/x-pack/test/token_api_integration/auth/logout.js b/x-pack/test/token_api_integration/auth/logout.js index 9063488681958..fa7a0606c3770 100644 --- a/x-pack/test/token_api_integration/auth/logout.js +++ b/x-pack/test/token_api_integration/auth/logout.js @@ -16,7 +16,7 @@ export default function ({ getService }) { async function createSessionCookie() { const response = await supertest - .post('/api/security/v1/login') + .post('/internal/security/login') .set('kbn-xsrf', 'true') .send({ username: 'elastic', password: 'changeme' }); @@ -33,7 +33,7 @@ export default function ({ getService }) { const cookie = await createSessionCookie(); await supertest - .get('/api/security/v1/logout') + .get('/api/security/logout') .set('cookie', cookie.cookieString()) .expect(302) .expect('location', '/login?msg=LOGGED_OUT'); @@ -43,7 +43,7 @@ export default function ({ getService }) { const cookie = await createSessionCookie(); const response = await supertest - .get('/api/security/v1/logout') + .get('/api/security/logout') .set('cookie', cookie.cookieString()); const newCookie = extractSessionCookie(response); @@ -60,12 +60,12 @@ export default function ({ getService }) { // destroy it await supertest - .get('/api/security/v1/logout') + .get('/api/security/logout') .set('cookie', cookie.cookieString()); // verify that the cookie no longer works await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'true') .set('cookie', cookie.cookieString()) .expect(400); diff --git a/x-pack/test/token_api_integration/auth/session.js b/x-pack/test/token_api_integration/auth/session.js index 8a9f1d7a3f229..6e8e8c01f3da6 100644 --- a/x-pack/test/token_api_integration/auth/session.js +++ b/x-pack/test/token_api_integration/auth/session.js @@ -19,7 +19,7 @@ export default function ({ getService }) { async function createSessionCookie() { const response = await supertest - .post('/api/security/v1/login') + .post('/internal/security/login') .set('kbn-xsrf', 'true') .send({ username: 'elastic', password: 'changeme' }); @@ -36,7 +36,7 @@ export default function ({ getService }) { const cookie = await createSessionCookie(); await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'true') .set('cookie', cookie.cookieString()) .expect(200); @@ -47,14 +47,14 @@ export default function ({ getService }) { // try it once await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'true') .set('cookie', cookie.cookieString()) .expect(200); // try it again to verity it isn't invalidated after a single request await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'true') .set('cookie', cookie.cookieString()) .expect(200); @@ -85,7 +85,7 @@ export default function ({ getService }) { // This api call should succeed and automatically refresh token. Returned cookie will contain // the new access and refresh token pair. const firstResponse = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'true') .set('cookie', originalCookie.cookieString()) .expect(200); @@ -96,7 +96,7 @@ export default function ({ getService }) { // Request with old cookie should return another valid cookie we can use to authenticate requests // if it happens within 60 seconds of the refresh token being used const secondResponse = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'true') .set('Cookie', originalCookie.cookieString()) .expect(200); @@ -110,14 +110,14 @@ export default function ({ getService }) { // The first new cookie should authenticate a subsequent request await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'true') .set('Cookie', firstNewCookie.cookieString()) .expect(200); // The second new cookie should authenticate a subsequent request await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'true') .set('Cookie', secondNewCookie.cookieString()) .expect(200); From b91c24c0b3b28863fb907f196c649dce41790a81 Mon Sep 17 00:00:00 2001 From: Brandon Morelli Date: Wed, 11 Dec 2019 09:53:44 -0800 Subject: [PATCH 36/40] docs: unknown route (#52703) --- docs/apm/troubleshooting.asciidoc | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/docs/apm/troubleshooting.asciidoc b/docs/apm/troubleshooting.asciidoc index 0e902e3608e72..ec0863b09d653 100644 --- a/docs/apm/troubleshooting.asciidoc +++ b/docs/apm/troubleshooting.asciidoc @@ -42,3 +42,22 @@ Finally, this problem can also occur if you've changed the index name that you w The default index pattern can be found {apm-server-ref}/elasticsearch-output.html#index-option-es[here]. If you change this setting, you must also configure the `setup.template.name` and `setup.template.pattern` options. See {apm-server-ref}/configuration-template.html[Load the Elasticsearch index template]. + +==== Unknown route + +The {apm-app-ref}/transactions.html[transaction overview] will only display helpful information +when the transactions in your services are named correctly. +If you're seeing "GET unknown route" or "unknown route" in the APM app, +it could be a sign that something isn't working like it should. + +Elastic APM Agents come with built-in support for popular frameworks out-of-the-box. +This means, among other things, that the Agent will try to automatically name HTTP requests. +As an example, the Node.js Agent uses the route that handled the request, while the Java Agent uses the Servlet name. + +"Unknown route" indicates that the Agent can't determine what to name the request, +perhaps because the technology you're using isn't supported, the Agent has been installed incorrectly, +or because something is happening to the request that the Agent doesn't understand. + +To resolve this, you'll need to head over to the relevant {apm-agents-ref}[Agent documentation]. +Specifically, view the Agent's supported technologies page. +You can also use the Agent's public API to manually set a name for the transaction. From 8395596159d649947091f07541d2129bcfb99227 Mon Sep 17 00:00:00 2001 From: Lukas Olson Date: Wed, 11 Dec 2019 10:57:19 -0700 Subject: [PATCH 37/40] [Search service] Add timeout parameter from config to requests (#52352) * Add timeout parameter to requests * export SharedGlobalConfig from `core/server` --- .../core/server/kibana-plugin-server.md | 1 + ...kibana-plugin-server.sharedglobalconfig.md | 16 +++++++++++++ src/core/server/index.ts | 1 + src/core/server/plugins/types.ts | 3 +++ src/core/server/server.api.md | 12 +++++++++- src/plugins/data/server/search/create_api.ts | 2 +- .../es_search/es_search_strategy.test.ts | 23 ++++++++++++------- .../search/es_search/es_search_strategy.ts | 3 +++ .../data/server/search/i_search_context.ts | 5 +++- .../data/server/search/search_service.ts | 5 ++++ 10 files changed, 60 insertions(+), 11 deletions(-) create mode 100644 docs/development/core/server/kibana-plugin-server.sharedglobalconfig.md diff --git a/docs/development/core/server/kibana-plugin-server.md b/docs/development/core/server/kibana-plugin-server.md index fceabd1237665..1506fdbb2b37f 100644 --- a/docs/development/core/server/kibana-plugin-server.md +++ b/docs/development/core/server/kibana-plugin-server.md @@ -197,5 +197,6 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SavedObjectsClientContract](./kibana-plugin-server.savedobjectsclientcontract.md) | Saved Objects is Kibana's data persisentence mechanism allowing plugins to use Elasticsearch for storing plugin state.\#\# SavedObjectsClient errorsSince the SavedObjectsClient has its hands in everything we are a little paranoid about the way we present errors back to to application code. Ideally, all errors will be either:1. Caused by bad implementation (ie. undefined is not a function) and as such unpredictable 2. An error that has been classified and decorated appropriately by the decorators in [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md)Type 1 errors are inevitable, but since all expected/handle-able errors should be Type 2 the isXYZError() helpers exposed at SavedObjectsErrorHelpers should be used to understand and manage error responses from the SavedObjectsClient.Type 2 errors are decorated versions of the source error, so if the elasticsearch client threw an error it will be decorated based on its type. That means that rather than looking for error.body.error.type or doing substring checks on error.body.error.reason, just use the helpers to understand the meaning of the error:\`\`\`js if (SavedObjectsErrorHelpers.isNotFoundError(error)) { // handle 404 }if (SavedObjectsErrorHelpers.isNotAuthorizedError(error)) { // 401 handling should be automatic, but in case you wanted to know }// always rethrow the error unless you handle it throw error; \`\`\`\#\#\# 404s from missing indexFrom the perspective of application code and APIs the SavedObjectsClient is a black box that persists objects. One of the internal details that users have no control over is that we use an elasticsearch index for persistance and that index might be missing.At the time of writing we are in the process of transitioning away from the operating assumption that the SavedObjects index is always available. Part of this transition is handling errors resulting from an index missing. These used to trigger a 500 error in most cases, and in others cause 404s with different error messages.From my (Spencer) perspective, a 404 from the SavedObjectsApi is a 404; The object the request/call was targeting could not be found. This is why \#14141 takes special care to ensure that 404 errors are generic and don't distinguish between index missing or document missing.\#\#\# 503s from missing indexUnlike all other methods, create requests are supposed to succeed even when the Kibana index does not exist because it will be automatically created by elasticsearch. When that is not the case it is because Elasticsearch's action.auto_create_index setting prevents it from being created automatically so we throw a special 503 with the intention of informing the user that their Elasticsearch settings need to be updated.See [SavedObjectsClient](./kibana-plugin-server.savedobjectsclient.md) See [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md) | | [SavedObjectsClientFactory](./kibana-plugin-server.savedobjectsclientfactory.md) | Describes the factory used to create instances of the Saved Objects Client. | | [SavedObjectsClientWrapperFactory](./kibana-plugin-server.savedobjectsclientwrapperfactory.md) | Describes the factory used to create instances of Saved Objects Client Wrappers. | +| [SharedGlobalConfig](./kibana-plugin-server.sharedglobalconfig.md) | | | [UiSettingsType](./kibana-plugin-server.uisettingstype.md) | UI element type to represent the settings. | diff --git a/docs/development/core/server/kibana-plugin-server.sharedglobalconfig.md b/docs/development/core/server/kibana-plugin-server.sharedglobalconfig.md new file mode 100644 index 0000000000000..418d406d4c890 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.sharedglobalconfig.md @@ -0,0 +1,16 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SharedGlobalConfig](./kibana-plugin-server.sharedglobalconfig.md) + +## SharedGlobalConfig type + + +Signature: + +```typescript +export declare type SharedGlobalConfig = RecursiveReadonly<{ + kibana: Pick; + elasticsearch: Pick; + path: Pick; +}>; +``` diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 57156322e2849..51444a76f1737 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -143,6 +143,7 @@ export { PluginInitializerContext, PluginManifest, PluginName, + SharedGlobalConfig, } from './plugins'; export { diff --git a/src/core/server/plugins/types.ts b/src/core/server/plugins/types.ts index b4c8c98864263..36205cb7f047b 100644 --- a/src/core/server/plugins/types.ts +++ b/src/core/server/plugins/types.ts @@ -206,6 +206,9 @@ export const SharedGlobalConfigKeys = { path: ['data'] as const, }; +/** + * @public + */ export type SharedGlobalConfig = RecursiveReadonly<{ kibana: Pick; elasticsearch: Pick; diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index c855e04e420f7..142332d613dc9 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -1748,6 +1748,13 @@ export interface SessionStorageFactory { asScoped: (request: KibanaRequest) => SessionStorage; } +// @public (undocumented) +export type SharedGlobalConfig = RecursiveReadonly_2<{ + kibana: Pick; + elasticsearch: Pick; + path: Pick; +}>; + // @public export interface UiSettingsParams { category?: string[]; @@ -1785,6 +1792,9 @@ export const validBodyOutput: readonly ["data", "stream"]; // // src/core/server/http/router/response.ts:316:3 - (ae-forgotten-export) The symbol "KibanaResponse" needs to be exported by the entry point index.d.ts // src/core/server/plugins/plugins_service.ts:43:5 - (ae-forgotten-export) The symbol "InternalPluginInfo" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:228:15 - (ae-forgotten-export) The symbol "SharedGlobalConfig" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:213:3 - (ae-forgotten-export) The symbol "KibanaConfigType" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:213:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:214:3 - (ae-forgotten-export) The symbol "ElasticsearchConfigType" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:215:3 - (ae-forgotten-export) The symbol "PathConfigType" needs to be exported by the entry point index.d.ts ``` diff --git a/src/plugins/data/server/search/create_api.ts b/src/plugins/data/server/search/create_api.ts index 2a874869526d7..e1613103ac399 100644 --- a/src/plugins/data/server/search/create_api.ts +++ b/src/plugins/data/server/search/create_api.ts @@ -38,7 +38,7 @@ export function createApi({ } // Give providers access to other search strategies by injecting this function const strategy = await strategyProvider(caller, api.search); - return strategy.search(request); + return strategy.search(request, options); }, }; return api; diff --git a/src/plugins/data/server/search/es_search/es_search_strategy.test.ts b/src/plugins/data/server/search/es_search/es_search_strategy.test.ts index 7b725a47aa13b..99ccb4dcbebab 100644 --- a/src/plugins/data/server/search/es_search/es_search_strategy.test.ts +++ b/src/plugins/data/server/search/es_search/es_search_strategy.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { coreMock } from '../../../../../core/server/mocks'; +import { coreMock, pluginInitializerContextConfigMock } from '../../../../../core/server/mocks'; import { esSearchStrategyProvider } from './es_search_strategy'; describe('ES search strategy', () => { @@ -31,6 +31,7 @@ describe('ES search strategy', () => { }, }); const mockSearch = jest.fn(); + const mockConfig$ = pluginInitializerContextConfigMock({}).legacy.globalConfig$; beforeEach(() => { mockApiCaller.mockClear(); @@ -41,6 +42,7 @@ describe('ES search strategy', () => { const esSearch = esSearchStrategyProvider( { core: mockCoreSetup, + config$: mockConfig$, }, mockApiCaller, mockSearch @@ -49,11 +51,12 @@ describe('ES search strategy', () => { expect(typeof esSearch.search).toBe('function'); }); - it('logs the response if `debug` is set to `true`', () => { + it('logs the response if `debug` is set to `true`', async () => { const spy = jest.spyOn(console, 'log'); const esSearch = esSearchStrategyProvider( { core: mockCoreSetup, + config$: mockConfig$, }, mockApiCaller, mockSearch @@ -61,43 +64,46 @@ describe('ES search strategy', () => { expect(spy).not.toBeCalled(); - esSearch.search({ params: {}, debug: true }); + await esSearch.search({ params: {}, debug: true }); expect(spy).toBeCalled(); }); - it('calls the API caller with the params with defaults', () => { + it('calls the API caller with the params with defaults', async () => { const params = { index: 'logstash-*' }; const esSearch = esSearchStrategyProvider( { core: mockCoreSetup, + config$: mockConfig$, }, mockApiCaller, mockSearch ); - esSearch.search({ params }); + await esSearch.search({ params }); expect(mockApiCaller).toBeCalled(); expect(mockApiCaller.mock.calls[0][0]).toBe('search'); expect(mockApiCaller.mock.calls[0][1]).toEqual({ ...params, + timeout: '0ms', ignoreUnavailable: true, restTotalHitsAsInt: true, }); }); - it('calls the API caller with overridden defaults', () => { - const params = { index: 'logstash-*', ignoreUnavailable: false }; + it('calls the API caller with overridden defaults', async () => { + const params = { index: 'logstash-*', ignoreUnavailable: false, timeout: '1000ms' }; const esSearch = esSearchStrategyProvider( { core: mockCoreSetup, + config$: mockConfig$, }, mockApiCaller, mockSearch ); - esSearch.search({ params }); + await esSearch.search({ params }); expect(mockApiCaller).toBeCalled(); expect(mockApiCaller.mock.calls[0][0]).toBe('search'); @@ -112,6 +118,7 @@ describe('ES search strategy', () => { const esSearch = esSearchStrategyProvider( { core: mockCoreSetup, + config$: mockConfig$, }, mockApiCaller, mockSearch diff --git a/src/plugins/data/server/search/es_search/es_search_strategy.ts b/src/plugins/data/server/search/es_search/es_search_strategy.ts index c5fc1d9d3a11c..20bc964effc02 100644 --- a/src/plugins/data/server/search/es_search/es_search_strategy.ts +++ b/src/plugins/data/server/search/es_search/es_search_strategy.ts @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +import { first } from 'rxjs/operators'; import { APICaller } from 'kibana/server'; import { SearchResponse } from 'elasticsearch'; import { ES_SEARCH_STRATEGY } from '../../../common/search'; @@ -28,7 +29,9 @@ export const esSearchStrategyProvider: TSearchStrategyProvider => { return { search: async (request, options) => { + const config = await context.config$.pipe(first()).toPromise(); const params = { + timeout: `${config.elasticsearch.shardTimeout.asMilliseconds()}ms`, ignoreUnavailable: true, // Don't fail if the index/indices don't exist restTotalHitsAsInt: true, // Get the number of hits as an int rather than a range ...request.params, diff --git a/src/plugins/data/server/search/i_search_context.ts b/src/plugins/data/server/search/i_search_context.ts index 5f2df5d8e819e..9d9de055d994f 100644 --- a/src/plugins/data/server/search/i_search_context.ts +++ b/src/plugins/data/server/search/i_search_context.ts @@ -16,8 +16,11 @@ * specific language governing permissions and limitations * under the License. */ -import { CoreSetup } from '../../../../core/server'; + +import { Observable } from 'rxjs'; +import { CoreSetup, SharedGlobalConfig } from '../../../../core/server'; export interface ISearchContext { core: CoreSetup; + config$: Observable; } diff --git a/src/plugins/data/server/search/search_service.ts b/src/plugins/data/server/search/search_service.ts index 3409a72326121..8ca314ad7bfd8 100644 --- a/src/plugins/data/server/search/search_service.ts +++ b/src/plugins/data/server/search/search_service.ts @@ -83,6 +83,11 @@ export class SearchService implements Plugin { }; api.registerSearchStrategyContext(this.initializerContext.opaqueId, 'core', () => core); + api.registerSearchStrategyContext( + this.initializerContext.opaqueId, + 'config$', + () => this.initializerContext.config.legacy.globalConfig$ + ); // ES search capabilities are written in a way that it could easily be a separate plugin, // however these two plugins are tightly coupled due to the default search strategy using From 19fec54e0b9ad7043fd764b03ed6e8f044f920fb Mon Sep 17 00:00:00 2001 From: Poff Poffenberger Date: Wed, 11 Dec 2019 11:59:44 -0600 Subject: [PATCH 38/40] [Canvas] Add NP routing for custom elements (#52561) * [Canvas] Add NP routing for custom elements * Remove unused type * Cleanup * Adding await to delete * Cleanup --- .../canvas/server/routes/custom_elements.ts | 192 ------------------ .../plugins/canvas/server/routes/index.ts | 2 - .../routes/custom_elements/create.test.ts | 102 ++++++++++ .../server/routes/custom_elements/create.ts | 54 +++++ .../custom_element_attributes.ts | 12 ++ .../custom_elements/custom_element_schema.ts | 26 +++ .../routes/custom_elements/delete.test.ts | 81 ++++++++ .../server/routes/custom_elements/delete.ts | 32 +++ .../routes/custom_elements/find.test.ts | 113 +++++++++++ .../server/routes/custom_elements/find.ts | 72 +++++++ .../server/routes/custom_elements/get.test.ts | 109 ++++++++++ .../server/routes/custom_elements/get.ts | 41 ++++ .../server/routes/custom_elements/index.ts | 20 ++ .../routes/custom_elements/update.test.ts | 164 +++++++++++++++ .../server/routes/custom_elements/update.ts | 63 ++++++ x-pack/plugins/canvas/server/routes/index.ts | 2 + .../routes/{workpad => }/ok_response.ts | 0 .../canvas/server/routes/workpad/create.ts | 8 +- .../canvas/server/routes/workpad/delete.ts | 2 +- .../canvas/server/routes/workpad/get.ts | 7 +- .../server/routes/workpad/update.test.ts | 2 +- .../canvas/server/routes/workpad/update.ts | 9 +- .../routes/workpad/workpad_attributes.ts | 11 + 23 files changed, 909 insertions(+), 215 deletions(-) delete mode 100644 x-pack/legacy/plugins/canvas/server/routes/custom_elements.ts create mode 100644 x-pack/plugins/canvas/server/routes/custom_elements/create.test.ts create mode 100644 x-pack/plugins/canvas/server/routes/custom_elements/create.ts create mode 100644 x-pack/plugins/canvas/server/routes/custom_elements/custom_element_attributes.ts create mode 100644 x-pack/plugins/canvas/server/routes/custom_elements/custom_element_schema.ts create mode 100644 x-pack/plugins/canvas/server/routes/custom_elements/delete.test.ts create mode 100644 x-pack/plugins/canvas/server/routes/custom_elements/delete.ts create mode 100644 x-pack/plugins/canvas/server/routes/custom_elements/find.test.ts create mode 100644 x-pack/plugins/canvas/server/routes/custom_elements/find.ts create mode 100644 x-pack/plugins/canvas/server/routes/custom_elements/get.test.ts create mode 100644 x-pack/plugins/canvas/server/routes/custom_elements/get.ts create mode 100644 x-pack/plugins/canvas/server/routes/custom_elements/index.ts create mode 100644 x-pack/plugins/canvas/server/routes/custom_elements/update.test.ts create mode 100644 x-pack/plugins/canvas/server/routes/custom_elements/update.ts rename x-pack/plugins/canvas/server/routes/{workpad => }/ok_response.ts (100%) create mode 100644 x-pack/plugins/canvas/server/routes/workpad/workpad_attributes.ts diff --git a/x-pack/legacy/plugins/canvas/server/routes/custom_elements.ts b/x-pack/legacy/plugins/canvas/server/routes/custom_elements.ts deleted file mode 100644 index 3fe78befd2f50..0000000000000 --- a/x-pack/legacy/plugins/canvas/server/routes/custom_elements.ts +++ /dev/null @@ -1,192 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import boom from 'boom'; -import { omit } from 'lodash'; -import { SavedObjectsClientContract } from 'src/core/server'; - -import { API_ROUTE_CUSTOM_ELEMENT, CUSTOM_ELEMENT_TYPE } from '../../common/lib/constants'; -import { getId } from '../../public/lib/get_id'; -// @ts-ignore Untyped Local -import { formatResponse as formatRes } from '../lib/format_response'; -import { CustomElement } from '../../types'; - -import { CoreSetup } from '../shim'; - -// Exclude ID attribute for the type used for SavedObjectClient -type CustomElementAttributes = Pick> & { - '@timestamp': string; - '@created': string; -}; - -interface CustomElementRequestFacade { - getSavedObjectsClient: () => SavedObjectsClientContract; -} - -type CustomElementRequest = CustomElementRequestFacade & { - params: { - id: string; - }; - payload: CustomElement; -}; - -type FindCustomElementRequest = CustomElementRequestFacade & { - query: { - name: string; - page: number; - perPage: number; - }; -}; - -export function customElements( - route: CoreSetup['http']['route'], - elasticsearch: CoreSetup['elasticsearch'] -) { - // @ts-ignore: errors not on Cluster type - const { errors: esErrors } = elasticsearch.getCluster('data'); - - const routePrefix = API_ROUTE_CUSTOM_ELEMENT; - const formatResponse = formatRes(esErrors); - - const createCustomElement = (req: CustomElementRequest) => { - const savedObjectsClient = req.getSavedObjectsClient(); - - if (!req.payload) { - return Promise.reject(boom.badRequest('A custom element payload is required')); - } - - const now = new Date().toISOString(); - const { id, ...payload } = req.payload; - return savedObjectsClient.create( - CUSTOM_ELEMENT_TYPE, - { - ...payload, - '@timestamp': now, - '@created': now, - }, - { id: id || getId('custom-element') } - ); - }; - - const updateCustomElement = (req: CustomElementRequest, newPayload?: CustomElement) => { - const savedObjectsClient = req.getSavedObjectsClient(); - const { id } = req.params; - const payload = newPayload ? newPayload : req.payload; - - const now = new Date().toISOString(); - - return savedObjectsClient - .get(CUSTOM_ELEMENT_TYPE, id) - .then(element => { - // TODO: Using create with force over-write because of version conflict issues with update - return savedObjectsClient.create( - CUSTOM_ELEMENT_TYPE, - { - ...element.attributes, - ...omit(payload, 'id'), // never write the id property - '@timestamp': now, // always update the modified time - '@created': element.attributes['@created'], // ensure created is not modified - }, - { overwrite: true, id } - ); - }); - }; - - const deleteCustomElement = (req: CustomElementRequest) => { - const savedObjectsClient = req.getSavedObjectsClient(); - const { id } = req.params; - - return savedObjectsClient.delete(CUSTOM_ELEMENT_TYPE, id); - }; - - const findCustomElement = (req: FindCustomElementRequest) => { - const savedObjectsClient = req.getSavedObjectsClient(); - const { name, page, perPage } = req.query; - - return savedObjectsClient.find({ - type: CUSTOM_ELEMENT_TYPE, - sortField: '@timestamp', - sortOrder: 'desc', - search: name ? `${name}* | ${name}` : '*', - searchFields: ['name'], - fields: ['id', 'name', 'displayName', 'help', 'image', 'content', '@created', '@timestamp'], - page, - perPage, - }); - }; - - const getCustomElementById = (req: CustomElementRequest) => { - const savedObjectsClient = req.getSavedObjectsClient(); - const { id } = req.params; - return savedObjectsClient.get(CUSTOM_ELEMENT_TYPE, id); - }; - - // get custom element by id - route({ - method: 'GET', - path: `${routePrefix}/{id}`, - handler: (req: CustomElementRequest) => - getCustomElementById(req) - .then(obj => ({ id: obj.id, ...obj.attributes })) - .then(formatResponse) - .catch(formatResponse), - }); - - // create custom element - route({ - method: 'POST', - path: routePrefix, - // @ts-ignore config option missing on route method type - config: { payload: { allow: 'application/json', maxBytes: 26214400 } }, // 25MB payload limit - handler: (req: CustomElementRequest) => - createCustomElement(req) - .then(() => ({ ok: true })) - .catch(formatResponse), - }); - - // update custom element - route({ - method: 'PUT', - path: `${routePrefix}/{id}`, - // @ts-ignore config option missing on route method type - config: { payload: { allow: 'application/json', maxBytes: 26214400 } }, // 25MB payload limit - handler: (req: CustomElementRequest) => - updateCustomElement(req) - .then(() => ({ ok: true })) - .catch(formatResponse), - }); - - // delete custom element - route({ - method: 'DELETE', - path: `${routePrefix}/{id}`, - handler: (req: CustomElementRequest) => - deleteCustomElement(req) - .then(() => ({ ok: true })) - .catch(formatResponse), - }); - - // find custom elements - route({ - method: 'GET', - path: `${routePrefix}/find`, - handler: (req: FindCustomElementRequest) => - findCustomElement(req) - .then(formatResponse) - .then(resp => { - return { - total: resp.total, - customElements: resp.saved_objects.map(hit => ({ id: hit.id, ...hit.attributes })), - }; - }) - .catch(() => { - return { - total: 0, - customElements: [], - }; - }), - }); -} diff --git a/x-pack/legacy/plugins/canvas/server/routes/index.ts b/x-pack/legacy/plugins/canvas/server/routes/index.ts index 515d5b5e895ed..2f6b706fc7edb 100644 --- a/x-pack/legacy/plugins/canvas/server/routes/index.ts +++ b/x-pack/legacy/plugins/canvas/server/routes/index.ts @@ -5,12 +5,10 @@ */ import { esFields } from './es_fields'; -import { customElements } from './custom_elements'; import { shareableWorkpads } from './shareables'; import { CoreSetup } from '../shim'; export function routes(setup: CoreSetup): void { - customElements(setup.http.route, setup.elasticsearch); esFields(setup.http.route, setup.elasticsearch); shareableWorkpads(setup.http.route); } diff --git a/x-pack/plugins/canvas/server/routes/custom_elements/create.test.ts b/x-pack/plugins/canvas/server/routes/custom_elements/create.test.ts new file mode 100644 index 0000000000000..d3a69c01732fa --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/custom_elements/create.test.ts @@ -0,0 +1,102 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import sinon from 'sinon'; +import { + savedObjectsClientMock, + httpServiceMock, + httpServerMock, + loggingServiceMock, +} from 'src/core/server/mocks'; +import { CUSTOM_ELEMENT_TYPE } from '../../../../../legacy/plugins/canvas/common/lib/constants'; +import { initializeCreateCustomElementRoute } from './create'; +import { + IRouter, + kibanaResponseFactory, + RequestHandlerContext, + RequestHandler, +} from 'src/core/server'; + +const mockRouteContext = ({ + core: { + savedObjects: { + client: savedObjectsClientMock.create(), + }, + }, +} as unknown) as RequestHandlerContext; + +const mockedUUID = '123abc'; +const now = new Date(); +const nowIso = now.toISOString(); + +jest.mock('uuid/v4', () => jest.fn().mockReturnValue('123abc')); + +describe('POST custom element', () => { + let routeHandler: RequestHandler; + let clock: sinon.SinonFakeTimers; + + beforeEach(() => { + clock = sinon.useFakeTimers(now); + + const httpService = httpServiceMock.createSetupContract(); + + const router = httpService.createRouter('') as jest.Mocked; + initializeCreateCustomElementRoute({ + router, + logger: loggingServiceMock.create().get(), + }); + + routeHandler = router.post.mock.calls[0][1]; + }); + + afterEach(() => { + clock.restore(); + }); + + it(`returns 200 when the custom element is created`, async () => { + const mockCustomElement = { + displayName: 'My Custom Element', + }; + + const request = httpServerMock.createKibanaRequest({ + method: 'post', + path: 'api/canvas/custom-element', + body: mockCustomElement, + }); + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.status).toBe(200); + expect(response.payload).toEqual({ ok: true }); + expect(mockRouteContext.core.savedObjects.client.create).toBeCalledWith( + CUSTOM_ELEMENT_TYPE, + { + ...mockCustomElement, + '@timestamp': nowIso, + '@created': nowIso, + }, + { + id: `custom-element-${mockedUUID}`, + } + ); + }); + + it(`returns bad request if create is unsuccessful`, async () => { + const request = httpServerMock.createKibanaRequest({ + method: 'post', + path: 'api/canvas/custom-element', + body: {}, + }); + + (mockRouteContext.core.savedObjects.client.create as jest.Mock).mockImplementation(() => { + throw mockRouteContext.core.savedObjects.client.errors.createBadRequestError('bad request'); + }); + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.status).toBe(400); + }); +}); diff --git a/x-pack/plugins/canvas/server/routes/custom_elements/create.ts b/x-pack/plugins/canvas/server/routes/custom_elements/create.ts new file mode 100644 index 0000000000000..b882829124696 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/custom_elements/create.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { RouteInitializerDeps } from '../'; +import { + CUSTOM_ELEMENT_TYPE, + API_ROUTE_CUSTOM_ELEMENT, +} from '../../../../../legacy/plugins/canvas/common/lib/constants'; +import { getId } from '../../../../../legacy/plugins/canvas/public/lib/get_id'; +import { CustomElementSchema } from './custom_element_schema'; +import { CustomElementAttributes } from './custom_element_attributes'; +import { okResponse } from '../ok_response'; +import { catchErrorHandler } from '../catch_error_handler'; + +export function initializeCreateCustomElementRoute(deps: RouteInitializerDeps) { + const { router } = deps; + router.post( + { + path: `${API_ROUTE_CUSTOM_ELEMENT}`, + validate: { + body: CustomElementSchema, + }, + options: { + body: { + maxBytes: 26214400, // 25MB payload limit + accepts: ['application/json'], + }, + }, + }, + catchErrorHandler(async (context, request, response) => { + const customElement = request.body; + + const now = new Date().toISOString(); + const { id, ...payload } = customElement; + + await context.core.savedObjects.client.create( + CUSTOM_ELEMENT_TYPE, + { + ...payload, + '@timestamp': now, + '@created': now, + }, + { id: id || getId('custom-element') } + ); + + return response.ok({ + body: okResponse, + }); + }) + ); +} diff --git a/x-pack/plugins/canvas/server/routes/custom_elements/custom_element_attributes.ts b/x-pack/plugins/canvas/server/routes/custom_elements/custom_element_attributes.ts new file mode 100644 index 0000000000000..e76526eeeb27b --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/custom_elements/custom_element_attributes.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { CustomElement } from '../../../../../legacy/plugins/canvas/types'; + +// Exclude ID attribute for the type used for SavedObjectClient +export type CustomElementAttributes = Pick> & { + '@timestamp': string; + '@created': string; +}; diff --git a/x-pack/plugins/canvas/server/routes/custom_elements/custom_element_schema.ts b/x-pack/plugins/canvas/server/routes/custom_elements/custom_element_schema.ts new file mode 100644 index 0000000000000..956dccc5aaea2 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/custom_elements/custom_element_schema.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; + +export const CustomElementSchema = schema.object({ + '@created': schema.maybe(schema.string()), + '@timestamp': schema.maybe(schema.string()), + content: schema.string(), + displayName: schema.string(), + help: schema.maybe(schema.string()), + id: schema.string(), + image: schema.maybe(schema.string()), + name: schema.string(), + tags: schema.maybe(schema.arrayOf(schema.string())), +}); + +export const CustomElementUpdateSchema = schema.object({ + displayName: schema.string(), + help: schema.maybe(schema.string()), + image: schema.maybe(schema.string()), + name: schema.string(), +}); diff --git a/x-pack/plugins/canvas/server/routes/custom_elements/delete.test.ts b/x-pack/plugins/canvas/server/routes/custom_elements/delete.test.ts new file mode 100644 index 0000000000000..c108f2316db27 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/custom_elements/delete.test.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CUSTOM_ELEMENT_TYPE } from '../../../../../legacy/plugins/canvas/common/lib/constants'; +import { initializeDeleteCustomElementRoute } from './delete'; +import { + IRouter, + kibanaResponseFactory, + RequestHandlerContext, + RequestHandler, +} from 'src/core/server'; +import { + savedObjectsClientMock, + httpServiceMock, + httpServerMock, + loggingServiceMock, +} from 'src/core/server/mocks'; + +const mockRouteContext = ({ + core: { + savedObjects: { + client: savedObjectsClientMock.create(), + }, + }, +} as unknown) as RequestHandlerContext; + +describe('DELETE custom element', () => { + let routeHandler: RequestHandler; + + beforeEach(() => { + const httpService = httpServiceMock.createSetupContract(); + const router = httpService.createRouter('') as jest.Mocked; + initializeDeleteCustomElementRoute({ + router, + logger: loggingServiceMock.create().get(), + }); + + routeHandler = router.delete.mock.calls[0][1]; + }); + + it(`returns 200 ok when the custom element is deleted`, async () => { + const id = 'some-id'; + const request = httpServerMock.createKibanaRequest({ + method: 'delete', + path: `api/canvas/custom-element/${id}`, + params: { + id, + }, + }); + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.status).toBe(200); + expect(response.payload).toEqual({ ok: true }); + expect(mockRouteContext.core.savedObjects.client.delete).toBeCalledWith( + CUSTOM_ELEMENT_TYPE, + id + ); + }); + + it(`returns bad request if delete is unsuccessful`, async () => { + const request = httpServerMock.createKibanaRequest({ + method: 'delete', + path: `api/canvas/custom-element/some-id`, + params: { + id: 'some-id', + }, + }); + + (mockRouteContext.core.savedObjects.client.delete as jest.Mock).mockImplementationOnce(() => { + throw mockRouteContext.core.savedObjects.client.errors.createBadRequestError('bad request'); + }); + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.status).toBe(400); + }); +}); diff --git a/x-pack/plugins/canvas/server/routes/custom_elements/delete.ts b/x-pack/plugins/canvas/server/routes/custom_elements/delete.ts new file mode 100644 index 0000000000000..5867539b95b53 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/custom_elements/delete.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { RouteInitializerDeps } from '../'; +import { + CUSTOM_ELEMENT_TYPE, + API_ROUTE_CUSTOM_ELEMENT, +} from '../../../../../legacy/plugins/canvas/common/lib/constants'; +import { okResponse } from '../ok_response'; +import { catchErrorHandler } from '../catch_error_handler'; + +export function initializeDeleteCustomElementRoute(deps: RouteInitializerDeps) { + const { router } = deps; + router.delete( + { + path: `${API_ROUTE_CUSTOM_ELEMENT}/{id}`, + validate: { + params: schema.object({ + id: schema.string(), + }), + }, + }, + catchErrorHandler(async (context, request, response) => { + await context.core.savedObjects.client.delete(CUSTOM_ELEMENT_TYPE, request.params.id); + return response.ok({ body: okResponse }); + }) + ); +} diff --git a/x-pack/plugins/canvas/server/routes/custom_elements/find.test.ts b/x-pack/plugins/canvas/server/routes/custom_elements/find.test.ts new file mode 100644 index 0000000000000..6644d3b56c681 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/custom_elements/find.test.ts @@ -0,0 +1,113 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { initializeFindCustomElementsRoute } from './find'; +import { + IRouter, + kibanaResponseFactory, + RequestHandlerContext, + RequestHandler, +} from 'src/core/server'; +import { + savedObjectsClientMock, + httpServiceMock, + httpServerMock, + loggingServiceMock, +} from 'src/core/server/mocks'; + +const mockRouteContext = ({ + core: { + savedObjects: { + client: savedObjectsClientMock.create(), + }, + }, +} as unknown) as RequestHandlerContext; + +describe('Find custom element', () => { + let routeHandler: RequestHandler; + + beforeEach(() => { + const httpService = httpServiceMock.createSetupContract(); + const router = httpService.createRouter('') as jest.Mocked; + initializeFindCustomElementsRoute({ + router, + logger: loggingServiceMock.create().get(), + }); + + routeHandler = router.get.mock.calls[0][1]; + }); + + it(`returns 200 with the found custom elements`, async () => { + const name = 'something'; + const perPage = 10000; + const mockResults = { + total: 2, + saved_objects: [ + { id: 1, attributes: { key: 'value' } }, + { id: 2, attributes: { key: 'other-value' } }, + ], + }; + + const findMock = mockRouteContext.core.savedObjects.client.find as jest.Mock; + + findMock.mockResolvedValueOnce(mockResults); + + const request = httpServerMock.createKibanaRequest({ + method: 'get', + path: `api/canvas/custom-elements/find`, + query: { + name, + perPage, + }, + }); + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + expect(response.status).toBe(200); + expect(findMock.mock.calls[0][0].search).toBe(`${name}* | ${name}`); + expect(findMock.mock.calls[0][0].perPage).toBe(perPage); + + expect(response.payload).toMatchInlineSnapshot(` + Object { + "customElements": Array [ + Object { + "id": 1, + "key": "value", + }, + Object { + "id": 2, + "key": "other-value", + }, + ], + "total": 2, + } + `); + }); + + it(`returns 200 with empty results on error`, async () => { + (mockRouteContext.core.savedObjects.client.find as jest.Mock).mockImplementationOnce(() => { + throw new Error('generic error'); + }); + + const request = httpServerMock.createKibanaRequest({ + method: 'get', + path: `api/canvas/custom-elements/find`, + query: { + name: 'something', + perPage: 1000, + }, + }); + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.status).toBe(200); + expect(response.payload).toMatchInlineSnapshot(` + Object { + "customElements": Array [], + "total": 0, + } + `); + }); +}); diff --git a/x-pack/plugins/canvas/server/routes/custom_elements/find.ts b/x-pack/plugins/canvas/server/routes/custom_elements/find.ts new file mode 100644 index 0000000000000..5041ceb3e4711 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/custom_elements/find.ts @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { SavedObjectAttributes } from 'src/core/server'; +import { RouteInitializerDeps } from '../'; +import { + CUSTOM_ELEMENT_TYPE, + API_ROUTE_CUSTOM_ELEMENT, +} from '../../../../../legacy/plugins/canvas/common/lib/constants'; + +export function initializeFindCustomElementsRoute(deps: RouteInitializerDeps) { + const { router } = deps; + router.get( + { + path: `${API_ROUTE_CUSTOM_ELEMENT}/find`, + validate: { + query: schema.object({ + name: schema.string(), + page: schema.maybe(schema.number()), + perPage: schema.number(), + }), + }, + }, + async (context, request, response) => { + const savedObjectsClient = context.core.savedObjects.client; + const { name, page, perPage } = request.query; + + try { + const customElements = await savedObjectsClient.find({ + type: CUSTOM_ELEMENT_TYPE, + sortField: '@timestamp', + sortOrder: 'desc', + search: name ? `${name}* | ${name}` : '*', + searchFields: ['name'], + fields: [ + 'id', + 'name', + 'displayName', + 'help', + 'image', + 'content', + '@created', + '@timestamp', + ], + page, + perPage, + }); + + return response.ok({ + body: { + total: customElements.total, + customElements: customElements.saved_objects.map(hit => ({ + id: hit.id, + ...hit.attributes, + })), + }, + }); + } catch (error) { + return response.ok({ + body: { + total: 0, + customElements: [], + }, + }); + } + } + ); +} diff --git a/x-pack/plugins/canvas/server/routes/custom_elements/get.test.ts b/x-pack/plugins/canvas/server/routes/custom_elements/get.test.ts new file mode 100644 index 0000000000000..5e8d536f779a9 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/custom_elements/get.test.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CUSTOM_ELEMENT_TYPE } from '../../../../../legacy/plugins/canvas/common/lib/constants'; +import { initializeGetCustomElementRoute } from './get'; +import { + IRouter, + kibanaResponseFactory, + RequestHandlerContext, + RequestHandler, +} from 'src/core/server'; +import { + savedObjectsClientMock, + httpServiceMock, + httpServerMock, + loggingServiceMock, +} from 'src/core/server/mocks'; + +const mockRouteContext = ({ + core: { + savedObjects: { + client: savedObjectsClientMock.create(), + }, + }, +} as unknown) as RequestHandlerContext; + +describe('GET custom element', () => { + let routeHandler: RequestHandler; + + beforeEach(() => { + const httpService = httpServiceMock.createSetupContract(); + const router = httpService.createRouter('') as jest.Mocked; + initializeGetCustomElementRoute({ + router, + logger: loggingServiceMock.create().get(), + }); + + routeHandler = router.get.mock.calls[0][1]; + }); + + it(`returns 200 when the custom element is found`, async () => { + const request = httpServerMock.createKibanaRequest({ + method: 'get', + path: 'api/canvas/custom-element/123', + params: { + id: '123', + }, + }); + + const savedObjectsClient = savedObjectsClientMock.create(); + savedObjectsClient.get.mockResolvedValueOnce({ + id: '123', + type: CUSTOM_ELEMENT_TYPE, + attributes: { foo: true }, + references: [], + }); + + mockRouteContext.core.savedObjects.client = savedObjectsClient; + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.status).toBe(200); + expect(response.payload).toMatchInlineSnapshot(` + Object { + "foo": true, + "id": "123", + } + `); + + expect(savedObjectsClient.get.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "canvas-element", + "123", + ], + ] + `); + }); + + it('returns 404 if the custom element is not found', async () => { + const id = '123'; + const request = httpServerMock.createKibanaRequest({ + method: 'get', + path: 'api/canvas/custom-element/123', + params: { + id, + }, + }); + + const savedObjectsClient = savedObjectsClientMock.create(); + savedObjectsClient.get.mockImplementation(() => { + throw savedObjectsClient.errors.createGenericNotFoundError(CUSTOM_ELEMENT_TYPE, id); + }); + mockRouteContext.core.savedObjects.client = savedObjectsClient; + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.payload).toMatchInlineSnapshot(` + Object { + "error": "Not Found", + "message": "Saved object [canvas-element/123] not found", + "statusCode": 404, + } + `); + }); +}); diff --git a/x-pack/plugins/canvas/server/routes/custom_elements/get.ts b/x-pack/plugins/canvas/server/routes/custom_elements/get.ts new file mode 100644 index 0000000000000..f092b001e141f --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/custom_elements/get.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { RouteInitializerDeps } from '../'; +import { + CUSTOM_ELEMENT_TYPE, + API_ROUTE_CUSTOM_ELEMENT, +} from '../../../../../legacy/plugins/canvas/common/lib/constants'; +import { CustomElementAttributes } from './custom_element_attributes'; +import { catchErrorHandler } from '../catch_error_handler'; + +export function initializeGetCustomElementRoute(deps: RouteInitializerDeps) { + const { router } = deps; + router.get( + { + path: `${API_ROUTE_CUSTOM_ELEMENT}/{id}`, + validate: { + params: schema.object({ + id: schema.string(), + }), + }, + }, + catchErrorHandler(async (context, request, response) => { + const customElement = await context.core.savedObjects.client.get( + CUSTOM_ELEMENT_TYPE, + request.params.id + ); + + return response.ok({ + body: { + id: customElement.id, + ...customElement.attributes, + }, + }); + }) + ); +} diff --git a/x-pack/plugins/canvas/server/routes/custom_elements/index.ts b/x-pack/plugins/canvas/server/routes/custom_elements/index.ts new file mode 100644 index 0000000000000..ade641e491371 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/custom_elements/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { RouteInitializerDeps } from '../'; +import { initializeFindCustomElementsRoute } from './find'; +import { initializeGetCustomElementRoute } from './get'; +import { initializeCreateCustomElementRoute } from './create'; +import { initializeUpdateCustomElementRoute } from './update'; +import { initializeDeleteCustomElementRoute } from './delete'; + +export function initCustomElementsRoutes(deps: RouteInitializerDeps) { + initializeFindCustomElementsRoute(deps); + initializeGetCustomElementRoute(deps); + initializeCreateCustomElementRoute(deps); + initializeUpdateCustomElementRoute(deps); + initializeDeleteCustomElementRoute(deps); +} diff --git a/x-pack/plugins/canvas/server/routes/custom_elements/update.test.ts b/x-pack/plugins/canvas/server/routes/custom_elements/update.test.ts new file mode 100644 index 0000000000000..f21a9c25b6e64 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/custom_elements/update.test.ts @@ -0,0 +1,164 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import sinon from 'sinon'; +import { CustomElement } from '../../../../../legacy/plugins/canvas/types'; +import { CUSTOM_ELEMENT_TYPE } from '../../../../../legacy/plugins/canvas/common/lib/constants'; +import { initializeUpdateCustomElementRoute } from './update'; +import { + IRouter, + kibanaResponseFactory, + RequestHandlerContext, + RequestHandler, +} from 'src/core/server'; +import { + savedObjectsClientMock, + httpServiceMock, + httpServerMock, + loggingServiceMock, +} from 'src/core/server/mocks'; +import { okResponse } from '../ok_response'; + +const mockRouteContext = ({ + core: { + savedObjects: { + client: savedObjectsClientMock.create(), + }, + }, +} as unknown) as RequestHandlerContext; + +const now = new Date(); +const nowIso = now.toISOString(); + +jest.mock('uuid/v4', () => jest.fn().mockReturnValue('123abc')); + +type CustomElementPayload = CustomElement & { + '@timestamp': string; + '@created': string; +}; + +const customElement: CustomElementPayload = { + id: 'my-custom-element', + name: 'MyCustomElement', + displayName: 'My Wonderful Custom Element', + content: 'This is content', + tags: ['filter', 'graphic'], + '@created': '2019-02-08T18:35:23.029Z', + '@timestamp': '2019-02-08T18:35:23.029Z', +}; + +describe('PUT custom element', () => { + let routeHandler: RequestHandler; + let clock: sinon.SinonFakeTimers; + + beforeEach(() => { + clock = sinon.useFakeTimers(now); + + const httpService = httpServiceMock.createSetupContract(); + const router = httpService.createRouter('') as jest.Mocked; + initializeUpdateCustomElementRoute({ + router, + logger: loggingServiceMock.create().get(), + }); + + routeHandler = router.put.mock.calls[0][1]; + }); + + afterEach(() => { + jest.resetAllMocks(); + clock.restore(); + }); + + it(`returns 200 ok when the custom element is updated`, async () => { + const updatedCustomElement = { name: 'new name' }; + const { id, ...customElementAttributes } = customElement; + + const request = httpServerMock.createKibanaRequest({ + method: 'put', + path: `api/canvas/custom-element/${id}`, + params: { + id, + }, + body: updatedCustomElement, + }); + + const savedObjectsClient = savedObjectsClientMock.create(); + savedObjectsClient.get.mockResolvedValueOnce({ + id, + type: CUSTOM_ELEMENT_TYPE, + attributes: customElementAttributes as any, + references: [], + }); + + mockRouteContext.core.savedObjects.client = savedObjectsClient; + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.status).toBe(200); + expect(response.payload).toEqual(okResponse); + expect(mockRouteContext.core.savedObjects.client.create).toBeCalledWith( + CUSTOM_ELEMENT_TYPE, + { + ...customElementAttributes, + ...updatedCustomElement, + '@timestamp': nowIso, + '@created': customElement['@created'], + }, + { + overwrite: true, + id, + } + ); + }); + + it(`returns not found if existing custom element is not found`, async () => { + const request = httpServerMock.createKibanaRequest({ + method: 'put', + path: 'api/canvas/custom-element/some-id', + params: { + id: 'not-found', + }, + body: {}, + }); + + (mockRouteContext.core.savedObjects.client.get as jest.Mock).mockImplementationOnce(() => { + throw mockRouteContext.core.savedObjects.client.errors.createGenericNotFoundError( + 'not found' + ); + }); + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + expect(response.status).toBe(404); + }); + + it(`returns bad request if the write fails`, async () => { + const request = httpServerMock.createKibanaRequest({ + method: 'put', + path: 'api/canvas/custom-element/some-id', + params: { + id: 'some-id', + }, + body: {}, + }); + + const savedObjectsClient = savedObjectsClientMock.create(); + savedObjectsClient.get.mockResolvedValueOnce({ + id: 'some-id', + type: CUSTOM_ELEMENT_TYPE, + attributes: {}, + references: [], + }); + + mockRouteContext.core.savedObjects.client = savedObjectsClient; + (mockRouteContext.core.savedObjects.client.create as jest.Mock).mockImplementationOnce(() => { + throw mockRouteContext.core.savedObjects.client.errors.createBadRequestError('bad request'); + }); + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.status).toBe(400); + }); +}); diff --git a/x-pack/plugins/canvas/server/routes/custom_elements/update.ts b/x-pack/plugins/canvas/server/routes/custom_elements/update.ts new file mode 100644 index 0000000000000..51c363249dd79 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/custom_elements/update.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { omit } from 'lodash'; +import { RouteInitializerDeps } from '../'; +import { + CUSTOM_ELEMENT_TYPE, + API_ROUTE_CUSTOM_ELEMENT, +} from '../../../../../legacy/plugins/canvas/common/lib/constants'; +import { CustomElementUpdateSchema } from './custom_element_schema'; +import { CustomElementAttributes } from './custom_element_attributes'; +import { okResponse } from '../ok_response'; +import { catchErrorHandler } from '../catch_error_handler'; + +export function initializeUpdateCustomElementRoute(deps: RouteInitializerDeps) { + const { router } = deps; + router.put( + { + path: `${API_ROUTE_CUSTOM_ELEMENT}/{id}`, + validate: { + params: schema.object({ + id: schema.string(), + }), + body: CustomElementUpdateSchema, + }, + options: { + body: { + maxBytes: 26214400, // 25MB payload limit + accepts: ['application/json'], + }, + }, + }, + catchErrorHandler(async (context, request, response) => { + const payload = request.body; + const id = request.params.id; + + const now = new Date().toISOString(); + + const customElementObject = await context.core.savedObjects.client.get< + CustomElementAttributes + >(CUSTOM_ELEMENT_TYPE, id); + + await context.core.savedObjects.client.create( + CUSTOM_ELEMENT_TYPE, + { + ...customElementObject.attributes, + ...omit(payload, 'id'), // never write the id property + '@timestamp': now, + '@created': customElementObject.attributes['@created'], // ensure created is not modified + }, + { overwrite: true, id } + ); + + return response.ok({ + body: okResponse, + }); + }) + ); +} diff --git a/x-pack/plugins/canvas/server/routes/index.ts b/x-pack/plugins/canvas/server/routes/index.ts index 46873a6b32542..8b2d77d634760 100644 --- a/x-pack/plugins/canvas/server/routes/index.ts +++ b/x-pack/plugins/canvas/server/routes/index.ts @@ -6,6 +6,7 @@ import { IRouter, Logger } from 'src/core/server'; import { initWorkpadRoutes } from './workpad'; +import { initCustomElementsRoutes } from './custom_elements'; export interface RouteInitializerDeps { router: IRouter; @@ -14,4 +15,5 @@ export interface RouteInitializerDeps { export function initRoutes(deps: RouteInitializerDeps) { initWorkpadRoutes(deps); + initCustomElementsRoutes(deps); } diff --git a/x-pack/plugins/canvas/server/routes/workpad/ok_response.ts b/x-pack/plugins/canvas/server/routes/ok_response.ts similarity index 100% rename from x-pack/plugins/canvas/server/routes/workpad/ok_response.ts rename to x-pack/plugins/canvas/server/routes/ok_response.ts diff --git a/x-pack/plugins/canvas/server/routes/workpad/create.ts b/x-pack/plugins/canvas/server/routes/workpad/create.ts index be904356720b6..fc847d4816dbd 100644 --- a/x-pack/plugins/canvas/server/routes/workpad/create.ts +++ b/x-pack/plugins/canvas/server/routes/workpad/create.ts @@ -11,15 +11,11 @@ import { } from '../../../../../legacy/plugins/canvas/common/lib/constants'; import { CanvasWorkpad } from '../../../../../legacy/plugins/canvas/types'; import { getId } from '../../../../../legacy/plugins/canvas/public/lib/get_id'; +import { WorkpadAttributes } from './workpad_attributes'; import { WorkpadSchema } from './workpad_schema'; -import { okResponse } from './ok_response'; +import { okResponse } from '../ok_response'; import { catchErrorHandler } from '../catch_error_handler'; -export type WorkpadAttributes = Pick> & { - '@timestamp': string; - '@created': string; -}; - export function initializeCreateWorkpadRoute(deps: RouteInitializerDeps) { const { router } = deps; router.post( diff --git a/x-pack/plugins/canvas/server/routes/workpad/delete.ts b/x-pack/plugins/canvas/server/routes/workpad/delete.ts index 7adf11e7a887b..8de4ea0f9a27f 100644 --- a/x-pack/plugins/canvas/server/routes/workpad/delete.ts +++ b/x-pack/plugins/canvas/server/routes/workpad/delete.ts @@ -10,7 +10,7 @@ import { CANVAS_TYPE, API_ROUTE_WORKPAD, } from '../../../../../legacy/plugins/canvas/common/lib/constants'; -import { okResponse } from './ok_response'; +import { okResponse } from '../ok_response'; import { catchErrorHandler } from '../catch_error_handler'; export function initializeDeleteWorkpadRoute(deps: RouteInitializerDeps) { diff --git a/x-pack/plugins/canvas/server/routes/workpad/get.ts b/x-pack/plugins/canvas/server/routes/workpad/get.ts index 7a51006aa9f02..d7a5e77670f6e 100644 --- a/x-pack/plugins/canvas/server/routes/workpad/get.ts +++ b/x-pack/plugins/canvas/server/routes/workpad/get.ts @@ -10,14 +10,9 @@ import { CANVAS_TYPE, API_ROUTE_WORKPAD, } from '../../../../../legacy/plugins/canvas/common/lib/constants'; -import { CanvasWorkpad } from '../../../../../legacy/plugins/canvas/types'; +import { WorkpadAttributes } from './workpad_attributes'; import { catchErrorHandler } from '../catch_error_handler'; -export type WorkpadAttributes = Pick> & { - '@timestamp': string; - '@created': string; -}; - export function initializeGetWorkpadRoute(deps: RouteInitializerDeps) { const { router } = deps; router.get( diff --git a/x-pack/plugins/canvas/server/routes/workpad/update.test.ts b/x-pack/plugins/canvas/server/routes/workpad/update.test.ts index 492a6c98d71ee..de098dd9717ed 100644 --- a/x-pack/plugins/canvas/server/routes/workpad/update.test.ts +++ b/x-pack/plugins/canvas/server/routes/workpad/update.test.ts @@ -20,7 +20,7 @@ import { loggingServiceMock, } from 'src/core/server/mocks'; import { workpads } from '../../../../../legacy/plugins/canvas/__tests__/fixtures/workpads'; -import { okResponse } from './ok_response'; +import { okResponse } from '../ok_response'; const mockRouteContext = ({ core: { diff --git a/x-pack/plugins/canvas/server/routes/workpad/update.ts b/x-pack/plugins/canvas/server/routes/workpad/update.ts index 460aa174038ae..74dedb605472c 100644 --- a/x-pack/plugins/canvas/server/routes/workpad/update.ts +++ b/x-pack/plugins/canvas/server/routes/workpad/update.ts @@ -15,16 +15,11 @@ import { API_ROUTE_WORKPAD_STRUCTURES, API_ROUTE_WORKPAD_ASSETS, } from '../../../../../legacy/plugins/canvas/common/lib/constants'; -import { CanvasWorkpad } from '../../../../../legacy/plugins/canvas/types'; +import { WorkpadAttributes } from './workpad_attributes'; import { WorkpadSchema, WorkpadAssetSchema } from './workpad_schema'; -import { okResponse } from './ok_response'; +import { okResponse } from '../ok_response'; import { catchErrorHandler } from '../catch_error_handler'; -export type WorkpadAttributes = Pick> & { - '@timestamp': string; - '@created': string; -}; - const AssetsRecordSchema = schema.recordOf(schema.string(), WorkpadAssetSchema); const AssetPayloadSchema = schema.object({ diff --git a/x-pack/plugins/canvas/server/routes/workpad/workpad_attributes.ts b/x-pack/plugins/canvas/server/routes/workpad/workpad_attributes.ts new file mode 100644 index 0000000000000..2b7b6cca4ba2b --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/workpad/workpad_attributes.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { CanvasWorkpad } from '../../../../../legacy/plugins/canvas/types'; + +export type WorkpadAttributes = Pick> & { + '@timestamp': string; + '@created': string; +}; From f2b48910a024d9b0e96cffa4d83449a9aac10a9f Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Wed, 11 Dec 2019 21:25:18 +0300 Subject: [PATCH 39/40] [ui/public/utils] Move items into agg_types (#52744) Closes #51855 --- .../buckets/create_filter/ip_range.ts | 2 +- .../ui/public/agg_types/buckets/date_range.ts | 17 +++- .../ui/public/agg_types/buckets/geo_hash.ts | 19 +++-- .../ui/public/agg_types/buckets/ip_range.ts | 13 ++- .../agg_types/buckets/lib/cidr_mask.test.ts | 75 +++++++++++++++++ .../buckets/lib}/cidr_mask.ts | 3 +- .../buckets/lib/geo_utils.ts} | 47 +++++++---- .../metrics/lib/ordinal_suffix.test.ts} | 19 ++--- .../metrics/lib/ordinal_suffix.ts} | 4 +- .../public/agg_types/metrics/percentiles.ts | 2 +- .../ui/public/utils/__tests__/cidr_mask.ts | 84 ------------------- src/legacy/ui/public/utils/date_range.ts | 32 ------- src/legacy/ui/public/utils/ip_range.ts | 31 ------- .../default/controls/components/mask_list.tsx | 2 +- .../loader/pipeline_helpers/utilities.ts | 10 +-- 15 files changed, 159 insertions(+), 201 deletions(-) create mode 100644 src/legacy/ui/public/agg_types/buckets/lib/cidr_mask.test.ts rename src/legacy/ui/public/{utils => agg_types/buckets/lib}/cidr_mask.ts (96%) rename src/legacy/ui/public/{utils/geo_utils.js => agg_types/buckets/lib/geo_utils.ts} (55%) rename src/legacy/ui/public/{utils/__tests__/ordinal_suffix.js => agg_types/metrics/lib/ordinal_suffix.test.ts} (76%) rename src/legacy/ui/public/{utils/ordinal_suffix.js => agg_types/metrics/lib/ordinal_suffix.ts} (93%) delete mode 100644 src/legacy/ui/public/utils/__tests__/cidr_mask.ts delete mode 100644 src/legacy/ui/public/utils/date_range.ts delete mode 100644 src/legacy/ui/public/utils/ip_range.ts diff --git a/src/legacy/ui/public/agg_types/buckets/create_filter/ip_range.ts b/src/legacy/ui/public/agg_types/buckets/create_filter/ip_range.ts index 803f6d97ae42d..a513b8c782739 100644 --- a/src/legacy/ui/public/agg_types/buckets/create_filter/ip_range.ts +++ b/src/legacy/ui/public/agg_types/buckets/create_filter/ip_range.ts @@ -17,7 +17,7 @@ * under the License. */ -import { CidrMask } from '../../../utils/cidr_mask'; +import { CidrMask } from '../lib/cidr_mask'; import { IBucketAggConfig } from '../_bucket_agg_type'; import { IpRangeKey } from '../ip_range'; import { esFilters } from '../../../../../../plugins/data/public'; diff --git a/src/legacy/ui/public/agg_types/buckets/date_range.ts b/src/legacy/ui/public/agg_types/buckets/date_range.ts index 860d76ff2aa7b..4bc5814184d57 100644 --- a/src/legacy/ui/public/agg_types/buckets/date_range.ts +++ b/src/legacy/ui/public/agg_types/buckets/date_range.ts @@ -25,8 +25,6 @@ import { BucketAggType, IBucketAggConfig } from './_bucket_agg_type'; import { createFilterDateRange } from './create_filter/date_range'; import { DateRangesParamEditor } from '../../vis/editors/default/controls/date_ranges'; -// @ts-ignore -import { dateRange } from '../../utils/date_range'; import { KBN_FIELD_TYPES, TEXT_CONTEXT_TYPE, @@ -57,7 +55,7 @@ export const dateRangeBucketAgg = new BucketAggType({ fieldFormats.getDefaultInstance(KBN_FIELD_TYPES.DATE) ); const DateRangeFormat = FieldFormat.from(function(range: DateRangeKey) { - return dateRange.toString(range, formatter); + return convertDateRangeToString(range, formatter); }); return new DateRangeFormat(); }, @@ -114,3 +112,16 @@ export const dateRangeBucketAgg = new BucketAggType({ }, ], }); + +export const convertDateRangeToString = ( + { from, to }: DateRangeKey, + format: (val: any) => string +) => { + if (!from) { + return 'Before ' + format(to); + } else if (!to) { + return 'After ' + format(from); + } else { + return format(from) + ' to ' + format(to); + } +}; diff --git a/src/legacy/ui/public/agg_types/buckets/geo_hash.ts b/src/legacy/ui/public/agg_types/buckets/geo_hash.ts index 0acbaf4aa02a2..546e51c1492e8 100644 --- a/src/legacy/ui/public/agg_types/buckets/geo_hash.ts +++ b/src/legacy/ui/public/agg_types/buckets/geo_hash.ts @@ -28,8 +28,7 @@ import { PrecisionParamEditor } from '../../vis/editors/default/controls/precisi import { AggGroupNames } from '../../vis/editors/default/agg_groups'; import { KBN_FIELD_TYPES } from '../../../../../plugins/data/public'; -// @ts-ignore -import { geoContains, scaleBounds } from '../../utils/geo_utils'; +import { geoContains, scaleBounds, GeoBoundingBox } from './lib/geo_utils'; import { BUCKET_TYPES } from './bucket_agg_types'; const config = chrome.getUiSettingsClient(); @@ -70,15 +69,15 @@ function getPrecision(val: string) { return precision; } -const isOutsideCollar = (bounds: unknown, collar: MapCollar) => +const isOutsideCollar = (bounds: GeoBoundingBox, collar: MapCollar) => bounds && collar && !geoContains(collar, bounds); const geohashGridTitle = i18n.translate('common.ui.aggTypes.buckets.geohashGridTitle', { defaultMessage: 'Geohash', }); -interface MapCollar { - zoom: unknown; +interface MapCollar extends GeoBoundingBox { + zoom?: unknown; } export interface IBucketGeoHashGridAggConfig extends IBucketAggConfig { @@ -148,11 +147,13 @@ export const geoHashBucketAgg = new BucketAggType({ if (params.isFilteredByCollar && agg.getField()) { const { mapBounds, mapZoom } = params; if (mapBounds) { - let mapCollar; + let mapCollar: MapCollar; + if ( - !agg.lastMapCollar || - agg.lastMapCollar.zoom !== mapZoom || - isOutsideCollar(mapBounds, agg.lastMapCollar) + mapBounds && + (!agg.lastMapCollar || + agg.lastMapCollar.zoom !== mapZoom || + isOutsideCollar(mapBounds, agg.lastMapCollar)) ) { mapCollar = scaleBounds(mapBounds); mapCollar.zoom = mapZoom; diff --git a/src/legacy/ui/public/agg_types/buckets/ip_range.ts b/src/legacy/ui/public/agg_types/buckets/ip_range.ts index 35155a482734c..fefe69cbf8e79 100644 --- a/src/legacy/ui/public/agg_types/buckets/ip_range.ts +++ b/src/legacy/ui/public/agg_types/buckets/ip_range.ts @@ -23,7 +23,6 @@ import { npStart } from 'ui/new_platform'; import { BucketAggType, IBucketAggConfig } from './_bucket_agg_type'; import { IpRangeTypeParamEditor } from '../../vis/editors/default/controls/ip_range_type'; import { IpRangesParamEditor } from '../../vis/editors/default/controls/ip_ranges'; -import { ipRange } from '../../utils/ip_range'; import { BUCKET_TYPES } from './bucket_agg_types'; // @ts-ignore @@ -59,7 +58,7 @@ export const ipRangeBucketAgg = new BucketAggType({ fieldFormats.getDefaultInstance(KBN_FIELD_TYPES.IP) ); const IpRangeFormat = FieldFormat.from(function(range: IpRangeKey) { - return ipRange.toString(range, formatter); + return convertIPRangeToString(range, formatter); }); return new IpRangeFormat(); }, @@ -106,3 +105,13 @@ export const ipRangeBucketAgg = new BucketAggType({ }, ], }); + +export const convertIPRangeToString = (range: IpRangeKey, format: (val: any) => string) => { + if (range.type === 'mask') { + return format(range.mask); + } + const from = range.from ? format(range.from) : '-Infinity'; + const to = range.to ? format(range.to) : 'Infinity'; + + return `${from} to ${to}`; +}; diff --git a/src/legacy/ui/public/agg_types/buckets/lib/cidr_mask.test.ts b/src/legacy/ui/public/agg_types/buckets/lib/cidr_mask.test.ts new file mode 100644 index 0000000000000..01dd3ddf1b874 --- /dev/null +++ b/src/legacy/ui/public/agg_types/buckets/lib/cidr_mask.test.ts @@ -0,0 +1,75 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { CidrMask } from './cidr_mask'; + +describe('CidrMask', () => { + test('should throw errors with invalid CIDR masks', () => { + expect( + () => + // @ts-ignore + new CidrMask() + ).toThrowError(); + + expect(() => new CidrMask('')).toThrowError(); + expect(() => new CidrMask('hello, world')).toThrowError(); + expect(() => new CidrMask('0.0.0.0')).toThrowError(); + expect(() => new CidrMask('0.0.0.0/0')).toThrowError(); + expect(() => new CidrMask('0.0.0.0/33')).toThrowError(); + expect(() => new CidrMask('256.0.0.0/32')).toThrowError(); + expect(() => new CidrMask('0.0.0.0/32/32')).toThrowError(); + expect(() => new CidrMask('1.2.3/1')).toThrowError(); + expect(() => new CidrMask('0.0.0.0/123d')).toThrowError(); + }); + + test('should correctly grab IP address and prefix length', () => { + let mask = new CidrMask('0.0.0.0/1'); + expect(mask.initialAddress.toString()).toBe('0.0.0.0'); + expect(mask.prefixLength).toBe(1); + + mask = new CidrMask('128.0.0.1/31'); + expect(mask.initialAddress.toString()).toBe('128.0.0.1'); + expect(mask.prefixLength).toBe(31); + }); + + test('should calculate a range of IP addresses', () => { + let mask = new CidrMask('0.0.0.0/1'); + let range = mask.getRange(); + expect(range.from.toString()).toBe('0.0.0.0'); + expect(range.to.toString()).toBe('127.255.255.255'); + + mask = new CidrMask('1.2.3.4/2'); + range = mask.getRange(); + expect(range.from.toString()).toBe('0.0.0.0'); + expect(range.to.toString()).toBe('63.255.255.255'); + + mask = new CidrMask('67.129.65.201/27'); + range = mask.getRange(); + expect(range.from.toString()).toBe('67.129.65.192'); + expect(range.to.toString()).toBe('67.129.65.223'); + }); + + test('toString()', () => { + let mask = new CidrMask('.../1'); + expect(mask.toString()).toBe('0.0.0.0/1'); + + mask = new CidrMask('128.0.0.1/31'); + expect(mask.toString()).toBe('128.0.0.1/31'); + }); +}); diff --git a/src/legacy/ui/public/utils/cidr_mask.ts b/src/legacy/ui/public/agg_types/buckets/lib/cidr_mask.ts similarity index 96% rename from src/legacy/ui/public/utils/cidr_mask.ts rename to src/legacy/ui/public/agg_types/buckets/lib/cidr_mask.ts index 249c60dedfebf..aadbbc8c82276 100644 --- a/src/legacy/ui/public/utils/cidr_mask.ts +++ b/src/legacy/ui/public/agg_types/buckets/lib/cidr_mask.ts @@ -17,7 +17,8 @@ * under the License. */ -import { Ipv4Address } from '../../../../plugins/kibana_utils/public'; +import { Ipv4Address } from '../../../../../../plugins/kibana_utils/public'; + const NUM_BITS = 32; function throwError(mask: string) { diff --git a/src/legacy/ui/public/utils/geo_utils.js b/src/legacy/ui/public/agg_types/buckets/lib/geo_utils.ts similarity index 55% rename from src/legacy/ui/public/utils/geo_utils.js rename to src/legacy/ui/public/agg_types/buckets/lib/geo_utils.ts index 44b7670d16c11..639b6d1fbb03e 100644 --- a/src/legacy/ui/public/utils/geo_utils.js +++ b/src/legacy/ui/public/agg_types/buckets/lib/geo_utils.ts @@ -19,46 +19,57 @@ import _ from 'lodash'; -export function geoContains(collar, bounds) { - //test if bounds top_left is outside collar - if(bounds.top_left.lat > collar.top_left.lat || bounds.top_left.lon < collar.top_left.lon) { +interface GeoBoundingBoxCoordinate { + lat: number; + lon: number; +} + +export interface GeoBoundingBox { + top_left: GeoBoundingBoxCoordinate; + bottom_right: GeoBoundingBoxCoordinate; +} + +export function geoContains(collar: GeoBoundingBox, bounds: GeoBoundingBox) { + // test if bounds top_left is outside collar + if (bounds.top_left.lat > collar.top_left.lat || bounds.top_left.lon < collar.top_left.lon) { return false; } - //test if bounds bottom_right is outside collar - if(bounds.bottom_right.lat < collar.bottom_right.lat || bounds.bottom_right.lon > collar.bottom_right.lon) { + // test if bounds bottom_right is outside collar + if ( + bounds.bottom_right.lat < collar.bottom_right.lat || + bounds.bottom_right.lon > collar.bottom_right.lon + ) { return false; } - //both corners are inside collar so collar contains bounds + // both corners are inside collar so collar contains bounds return true; } -export function scaleBounds(bounds) { - if (!bounds) return; - - const scale = .5; // scale bounds by 50% +export function scaleBounds(bounds: GeoBoundingBox): GeoBoundingBox { + const scale = 0.5; // scale bounds by 50% const topLeft = bounds.top_left; const bottomRight = bounds.bottom_right; let latDiff = _.round(Math.abs(topLeft.lat - bottomRight.lat), 5); const lonDiff = _.round(Math.abs(bottomRight.lon - topLeft.lon), 5); - //map height can be zero when vis is first created - if(latDiff === 0) latDiff = lonDiff; + // map height can be zero when vis is first created + if (latDiff === 0) latDiff = lonDiff; const latDelta = latDiff * scale; let topLeftLat = _.round(topLeft.lat, 5) + latDelta; - if(topLeftLat > 90) topLeftLat = 90; + if (topLeftLat > 90) topLeftLat = 90; let bottomRightLat = _.round(bottomRight.lat, 5) - latDelta; - if(bottomRightLat < -90) bottomRightLat = -90; + if (bottomRightLat < -90) bottomRightLat = -90; const lonDelta = lonDiff * scale; let topLeftLon = _.round(topLeft.lon, 5) - lonDelta; - if(topLeftLon < -180) topLeftLon = -180; + if (topLeftLon < -180) topLeftLon = -180; let bottomRightLon = _.round(bottomRight.lon, 5) + lonDelta; - if(bottomRightLon > 180) bottomRightLon = 180; + if (bottomRightLon > 180) bottomRightLon = 180; return { - 'top_left': { lat: topLeftLat, lon: topLeftLon }, - 'bottom_right': { lat: bottomRightLat, lon: bottomRightLon } + top_left: { lat: topLeftLat, lon: topLeftLon }, + bottom_right: { lat: bottomRightLat, lon: bottomRightLon }, }; } diff --git a/src/legacy/ui/public/utils/__tests__/ordinal_suffix.js b/src/legacy/ui/public/agg_types/metrics/lib/ordinal_suffix.test.ts similarity index 76% rename from src/legacy/ui/public/utils/__tests__/ordinal_suffix.js rename to src/legacy/ui/public/agg_types/metrics/lib/ordinal_suffix.test.ts index dae12d41cfb5b..18ee6b4de3204 100644 --- a/src/legacy/ui/public/utils/__tests__/ordinal_suffix.js +++ b/src/legacy/ui/public/agg_types/metrics/lib/ordinal_suffix.test.ts @@ -17,11 +17,10 @@ * under the License. */ -import _ from 'lodash'; -import { ordinalSuffix } from '../ordinal_suffix'; -import expect from '@kbn/expect'; +import { forOwn } from 'lodash'; +import { ordinalSuffix } from './ordinal_suffix'; -describe('ordinal suffix util', function () { +describe('ordinal suffix util', () => { const checks = { 1: 'st', 2: 'nd', @@ -52,19 +51,19 @@ describe('ordinal suffix util', function () { 27: 'th', 28: 'th', 29: 'th', - 30: 'th' + 30: 'th', }; - _.forOwn(checks, function (expected, num) { + forOwn(checks, (expected, num: any) => { const int = parseInt(num, 10); const float = int + Math.random(); - it('knowns ' + int, function () { - expect(ordinalSuffix(num)).to.be(num + '' + expected); + it('knowns ' + int, () => { + expect(ordinalSuffix(num)).toBe(num + '' + expected); }); - it('knows ' + float, function () { - expect(ordinalSuffix(num)).to.be(num + '' + expected); + it('knows ' + float, () => { + expect(ordinalSuffix(num)).toBe(num + '' + expected); }); }); }); diff --git a/src/legacy/ui/public/utils/ordinal_suffix.js b/src/legacy/ui/public/agg_types/metrics/lib/ordinal_suffix.ts similarity index 93% rename from src/legacy/ui/public/utils/ordinal_suffix.js rename to src/legacy/ui/public/agg_types/metrics/lib/ordinal_suffix.ts index 64fb29c8ae534..21903995ebb2f 100644 --- a/src/legacy/ui/public/utils/ordinal_suffix.js +++ b/src/legacy/ui/public/agg_types/metrics/lib/ordinal_suffix.ts @@ -18,11 +18,11 @@ */ // adopted from http://stackoverflow.com/questions/3109978/php-display-number-with-ordinal-suffix -export function ordinalSuffix(num) { +export function ordinalSuffix(num: any): string { return num + '' + suffix(num); } -function suffix(num) { +function suffix(num: any): string { const int = Math.floor(parseFloat(num)); const hunth = int % 100; diff --git a/src/legacy/ui/public/agg_types/metrics/percentiles.ts b/src/legacy/ui/public/agg_types/metrics/percentiles.ts index 1a3606d677951..60946b6162b3b 100644 --- a/src/legacy/ui/public/agg_types/metrics/percentiles.ts +++ b/src/legacy/ui/public/agg_types/metrics/percentiles.ts @@ -28,7 +28,7 @@ import { getPercentileValue } from './percentiles_get_value'; import { PercentilesEditor } from '../../vis/editors/default/controls/percentiles'; // @ts-ignore -import { ordinalSuffix } from '../../utils/ordinal_suffix'; +import { ordinalSuffix } from './lib/ordinal_suffix'; export type IPercentileAggConfig = IResponseAggConfig; diff --git a/src/legacy/ui/public/utils/__tests__/cidr_mask.ts b/src/legacy/ui/public/utils/__tests__/cidr_mask.ts deleted file mode 100644 index 5277344448bd8..0000000000000 --- a/src/legacy/ui/public/utils/__tests__/cidr_mask.ts +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import expect from '@kbn/expect'; -import { CidrMask } from '../cidr_mask'; - -describe('CidrMask', () => { - it('should throw errors with invalid CIDR masks', () => { - expect( - () => - // @ts-ignore - new CidrMask() - ).to.throwError(); - - expect(() => new CidrMask('')).to.throwError(); - - expect(() => new CidrMask('hello, world')).to.throwError(); - - expect(() => new CidrMask('0.0.0.0')).to.throwError(); - - expect(() => new CidrMask('0.0.0.0/0')).to.throwError(); - - expect(() => new CidrMask('0.0.0.0/33')).to.throwError(); - - expect(() => new CidrMask('256.0.0.0/32')).to.throwError(); - - expect(() => new CidrMask('0.0.0.0/32/32')).to.throwError(); - - expect(() => new CidrMask('1.2.3/1')).to.throwError(); - - expect(() => new CidrMask('0.0.0.0/123d')).to.throwError(); - }); - - it('should correctly grab IP address and prefix length', () => { - let mask = new CidrMask('0.0.0.0/1'); - expect(mask.initialAddress.toString()).to.be('0.0.0.0'); - expect(mask.prefixLength).to.be(1); - - mask = new CidrMask('128.0.0.1/31'); - expect(mask.initialAddress.toString()).to.be('128.0.0.1'); - expect(mask.prefixLength).to.be(31); - }); - - it('should calculate a range of IP addresses', () => { - let mask = new CidrMask('0.0.0.0/1'); - let range = mask.getRange(); - expect(range.from.toString()).to.be('0.0.0.0'); - expect(range.to.toString()).to.be('127.255.255.255'); - - mask = new CidrMask('1.2.3.4/2'); - range = mask.getRange(); - expect(range.from.toString()).to.be('0.0.0.0'); - expect(range.to.toString()).to.be('63.255.255.255'); - - mask = new CidrMask('67.129.65.201/27'); - range = mask.getRange(); - expect(range.from.toString()).to.be('67.129.65.192'); - expect(range.to.toString()).to.be('67.129.65.223'); - }); - - it('toString()', () => { - let mask = new CidrMask('.../1'); - expect(mask.toString()).to.be('0.0.0.0/1'); - - mask = new CidrMask('128.0.0.1/31'); - expect(mask.toString()).to.be('128.0.0.1/31'); - }); -}); diff --git a/src/legacy/ui/public/utils/date_range.ts b/src/legacy/ui/public/utils/date_range.ts deleted file mode 100644 index ca44183b8d68b..0000000000000 --- a/src/legacy/ui/public/utils/date_range.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { DateRangeKey } from '../agg_types/buckets/date_range'; - -export const dateRange = { - toString({ from, to }: DateRangeKey, format: (val: any) => string) { - if (!from) { - return 'Before ' + format(to); - } else if (!to) { - return 'After ' + format(from); - } else { - return format(from) + ' to ' + format(to); - } - }, -}; diff --git a/src/legacy/ui/public/utils/ip_range.ts b/src/legacy/ui/public/utils/ip_range.ts deleted file mode 100644 index 45ce21709d68c..0000000000000 --- a/src/legacy/ui/public/utils/ip_range.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { IpRangeKey } from '../agg_types/buckets/ip_range'; - -export const ipRange = { - toString(range: IpRangeKey, format: (val: any) => string) { - if (range.type === 'mask') { - return format(range.mask); - } - const from = range.from ? format(range.from) : '-Infinity'; - const to = range.to ? format(range.to) : 'Infinity'; - return `${from} to ${to}`; - }, -}; diff --git a/src/legacy/ui/public/vis/editors/default/controls/components/mask_list.tsx b/src/legacy/ui/public/vis/editors/default/controls/components/mask_list.tsx index 7d964204ff90c..b48f07512332e 100644 --- a/src/legacy/ui/public/vis/editors/default/controls/components/mask_list.tsx +++ b/src/legacy/ui/public/vis/editors/default/controls/components/mask_list.tsx @@ -20,7 +20,7 @@ import React from 'react'; import { EuiFieldText, EuiFlexItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { CidrMask } from '../../../../../utils/cidr_mask'; +import { CidrMask } from '../../../../../agg_types/buckets/lib/cidr_mask'; import { InputList, InputListConfig, InputObject, InputModel, InputItem } from './input_list'; const EMPTY_STRING = ''; diff --git a/src/legacy/ui/public/visualize/loader/pipeline_helpers/utilities.ts b/src/legacy/ui/public/visualize/loader/pipeline_helpers/utilities.ts index 377e2cd97b72e..d754c1d395595 100644 --- a/src/legacy/ui/public/visualize/loader/pipeline_helpers/utilities.ts +++ b/src/legacy/ui/public/visualize/loader/pipeline_helpers/utilities.ts @@ -26,10 +26,8 @@ import { SerializedFieldFormat } from 'src/plugins/expressions/public'; import { IFieldFormatId, FieldFormat } from '../../../../../../plugins/data/public'; import { tabifyGetColumns } from '../../../agg_response/tabify/_get_columns'; -import { dateRange } from '../../../utils/date_range'; -import { ipRange } from '../../../utils/ip_range'; -import { DateRangeKey } from '../../../agg_types/buckets/date_range'; -import { IpRangeKey } from '../../../agg_types/buckets/ip_range'; +import { DateRangeKey, convertDateRangeToString } from '../../../agg_types/buckets/date_range'; +import { IpRangeKey, convertIPRangeToString } from '../../../agg_types/buckets/ip_range'; interface TermsFieldFormatParams { otherBucketLabel: string; @@ -120,14 +118,14 @@ export const getFormat: FormatFactory = mapping => { const nestedFormatter = mapping.params as SerializedFieldFormat; const DateRangeFormat = FieldFormat.from((range: DateRangeKey) => { const format = getFieldFormat(nestedFormatter.id, nestedFormatter.params); - return dateRange.toString(range, format.convert.bind(format)); + return convertDateRangeToString(range, format.convert.bind(format)); }); return new DateRangeFormat(); } else if (id === 'ip_range') { const nestedFormatter = mapping.params as SerializedFieldFormat; const IpRangeFormat = FieldFormat.from((range: IpRangeKey) => { const format = getFieldFormat(nestedFormatter.id, nestedFormatter.params); - return ipRange.toString(range, format.convert.bind(format)); + return convertIPRangeToString(range, format.convert.bind(format)); }); return new IpRangeFormat(); } else if (isTermsFieldFormat(mapping) && mapping.params) { From 7658e9c631d7a823028a37baa87ef39227a54b8d Mon Sep 17 00:00:00 2001 From: Dmitry Lemeshko Date: Wed, 11 Dec 2019 19:41:25 +0100 Subject: [PATCH 40/40] FTR: add 'throttle' option to cli (#33241) * [ftr/cli] add throttling option * [ftr/cli] add headless option, fix test --- .../kbn-test/src/functional_test_runner/cli.ts | 12 +++++++++++- .../functional_tests/cli/run_tests/cli.test.js | 16 ++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/packages/kbn-test/src/functional_test_runner/cli.ts b/packages/kbn-test/src/functional_test_runner/cli.ts index 36412961ce75b..11b9450f2af6e 100644 --- a/packages/kbn-test/src/functional_test_runner/cli.ts +++ b/packages/kbn-test/src/functional_test_runner/cli.ts @@ -57,6 +57,14 @@ export function runFtrCli() { } ); + if (flags.throttle) { + process.env.TEST_THROTTLE_NETWORK = '1'; + } + + if (flags.headless) { + process.env.TEST_BROWSER_HEADLESS = '1'; + } + let teardownRun = false; const teardown = async (err?: Error) => { if (teardownRun) return; @@ -97,7 +105,7 @@ export function runFtrCli() { { flags: { string: ['config', 'grep', 'exclude', 'include-tag', 'exclude-tag', 'kibana-install-dir'], - boolean: ['bail', 'invert', 'test-stats', 'updateBaselines'], + boolean: ['bail', 'invert', 'test-stats', 'updateBaselines', 'throttle', 'headless'], default: { config: 'test/functional/config.js', debug: true, @@ -113,6 +121,8 @@ export function runFtrCli() { --test-stats print the number of tests (included and excluded) to STDERR --updateBaselines replace baseline screenshots with whatever is generated from the test --kibana-install-dir directory where the Kibana install being tested resides + --throttle enable network throttling in Chrome browser + --headless run browser in headless mode `, }, } diff --git a/packages/kbn-test/src/functional_tests/cli/run_tests/cli.test.js b/packages/kbn-test/src/functional_tests/cli/run_tests/cli.test.js index 97b74a3b2b541..9f9a8f59fde9a 100644 --- a/packages/kbn-test/src/functional_tests/cli/run_tests/cli.test.js +++ b/packages/kbn-test/src/functional_tests/cli/run_tests/cli.test.js @@ -182,6 +182,22 @@ describe('run tests CLI', () => { expect(exitMock).not.toHaveBeenCalled(); }); + it('accepts network throttle option', async () => { + global.process.argv.push('--throttle'); + + await runTestsCli(['foo']); + + expect(exitMock).toHaveBeenCalledWith(1); + }); + + it('accepts headless option', async () => { + global.process.argv.push('--headless'); + + await runTestsCli(['foo']); + + expect(exitMock).toHaveBeenCalledWith(1); + }); + it('accepts extra server options', async () => { global.process.argv.push('--', '--server.foo=bar');