diff --git a/.eslintrc.js b/.eslintrc.js index 29528c249d279..d8b9a9d7cdd99 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1192,6 +1192,32 @@ module.exports = { }, }, + /** + * Osquery overrides + */ + { + extends: ['eslint:recommended', 'plugin:react/recommended'], + plugins: ['react'], + files: ['x-pack/plugins/osquery/**/*.{js,mjs,ts,tsx}'], + rules: { + 'arrow-body-style': ['error', 'as-needed'], + 'prefer-arrow-callback': 'error', + 'no-unused-vars': 'off', + 'react/prop-types': 'off', + }, + }, + { + // typescript and javascript for front end react performance + files: ['x-pack/plugins/osquery/public/**/!(*.test).{js,mjs,ts,tsx}'], + plugins: ['react', 'react-perf'], + rules: { + 'react-perf/jsx-no-new-object-as-prop': 'error', + 'react-perf/jsx-no-new-array-as-prop': 'error', + 'react-perf/jsx-no-new-function-as-prop': 'error', + 'react/jsx-no-bind': 'error', + }, + }, + /** * Prettier disables all conflicting rules, listing as last override so it takes precedence */ diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 0630937d5ac4b..01bbd59ba090f 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -330,6 +330,9 @@ x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @elastic/kib # Security Intelligence And Analytics /x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules @elastic/security-intelligence-analytics +# Security Asset Management +/x-pack/plugins/osquery @elastic/security-asset-management + # Design (at the bottom for specificity of SASS files) **/*.scss @elastic/kibana-design #CC# /packages/kbn-ui-framework/ @elastic/kibana-design diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index fd4ed75352b1f..fd7ca58d88994 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -460,6 +460,10 @@ Elastic. |This plugin provides shared components and services for use across observability solutions, as well as the observability landing page UI. +|{kib-repo}blob/{branch}/x-pack/plugins/osquery/README.md[osquery] +|This plugin adds extended support to Security Solution Fleet Osquery integration + + |{kib-repo}blob/{branch}/x-pack/plugins/painless_lab/README.md[painlessLab] |This plugin helps users learn how to use the Painless scripting language. diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 1a4fb390d0c17..a13976d148738 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -103,4 +103,5 @@ pageLoadAssetSize: stackAlerts: 29684 presentationUtil: 28545 spacesOss: 18817 + osquery: 107090 mapsFileUpload: 23775 diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index bfac437f3500a..f95c4286b3f26 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -38,6 +38,7 @@ "xpack.maps": ["plugins/maps"], "xpack.ml": ["plugins/ml"], "xpack.monitoring": ["plugins/monitoring"], + "xpack.osquery": ["plugins/osquery"], "xpack.painlessLab": "plugins/painless_lab", "xpack.remoteClusters": "plugins/remote_clusters", "xpack.reporting": ["plugins/reporting"], diff --git a/x-pack/plugins/osquery/README.md b/x-pack/plugins/osquery/README.md new file mode 100755 index 0000000000000..e0861fab2040b --- /dev/null +++ b/x-pack/plugins/osquery/README.md @@ -0,0 +1,9 @@ +# osquery + +This plugin adds extended support to Security Solution Fleet Osquery integration + +--- + +## Development + +See the [kibana contributing guide](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md) for instructions setting up your development environment. diff --git a/x-pack/plugins/osquery/common/constants.ts b/x-pack/plugins/osquery/common/constants.ts new file mode 100644 index 0000000000000..f6027d416beb1 --- /dev/null +++ b/x-pack/plugins/osquery/common/constants.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 const DEFAULT_MAX_TABLE_QUERY_SIZE = 10000; +export const DEFAULT_DARK_MODE = 'theme:darkMode'; diff --git a/x-pack/plugins/osquery/common/ecs/agent/index.ts b/x-pack/plugins/osquery/common/ecs/agent/index.ts new file mode 100644 index 0000000000000..6f29a2020c944 --- /dev/null +++ b/x-pack/plugins/osquery/common/ecs/agent/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 interface AgentEcs { + type?: string[]; +} diff --git a/x-pack/plugins/osquery/common/ecs/auditd/index.ts b/x-pack/plugins/osquery/common/ecs/auditd/index.ts new file mode 100644 index 0000000000000..7611e5424e297 --- /dev/null +++ b/x-pack/plugins/osquery/common/ecs/auditd/index.ts @@ -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. + */ + +export interface AuditdEcs { + result?: string[]; + session?: string[]; + data?: AuditdDataEcs; + summary?: SummaryEcs; + sequence?: string[]; +} + +export interface AuditdDataEcs { + acct?: string[]; + terminal?: string[]; + op?: string[]; +} + +export interface SummaryEcs { + actor?: PrimarySecondaryEcs; + object?: PrimarySecondaryEcs; + how?: string[]; + message_type?: string[]; + sequence?: string[]; +} + +export interface PrimarySecondaryEcs { + primary?: string[]; + secondary?: string[]; + type?: string[]; +} diff --git a/x-pack/plugins/osquery/common/ecs/cloud/index.ts b/x-pack/plugins/osquery/common/ecs/cloud/index.ts new file mode 100644 index 0000000000000..812b30bcc13f1 --- /dev/null +++ b/x-pack/plugins/osquery/common/ecs/cloud/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. + */ + +export interface CloudEcs { + instance?: CloudInstanceEcs; + machine?: CloudMachineEcs; + provider?: string[]; + region?: string[]; +} + +export interface CloudMachineEcs { + type?: string[]; +} + +export interface CloudInstanceEcs { + id?: string[]; +} diff --git a/x-pack/plugins/osquery/common/ecs/destination/index.ts b/x-pack/plugins/osquery/common/ecs/destination/index.ts new file mode 100644 index 0000000000000..be12e829108a9 --- /dev/null +++ b/x-pack/plugins/osquery/common/ecs/destination/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 { GeoEcs } from '../geo'; + +export interface DestinationEcs { + bytes?: number[]; + ip?: string[]; + port?: number[]; + domain?: string[]; + geo?: GeoEcs; + packets?: number[]; +} diff --git a/x-pack/plugins/osquery/common/ecs/dns/index.ts b/x-pack/plugins/osquery/common/ecs/dns/index.ts new file mode 100644 index 0000000000000..45192d03a10b6 --- /dev/null +++ b/x-pack/plugins/osquery/common/ecs/dns/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. + */ + +export interface DnsEcs { + question?: DnsQuestionEcs; + resolved_ip?: string[]; + response_code?: string[]; +} + +export interface DnsQuestionEcs { + name?: string[]; + type?: string[]; +} diff --git a/x-pack/plugins/osquery/common/ecs/ecs_fields/extend_map.test.ts b/x-pack/plugins/osquery/common/ecs/ecs_fields/extend_map.test.ts new file mode 100644 index 0000000000000..9ba22e83b4b4d --- /dev/null +++ b/x-pack/plugins/osquery/common/ecs/ecs_fields/extend_map.test.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 { extendMap } from './extend_map'; + +describe('ecs_fields test', () => { + describe('extendMap', () => { + test('it should extend a record', () => { + const osFieldsMap: Readonly> = { + 'os.platform': 'os.platform', + 'os.full': 'os.full', + 'os.family': 'os.family', + 'os.version': 'os.version', + 'os.kernel': 'os.kernel', + }; + const expected: Record = { + 'host.os.family': 'host.os.family', + 'host.os.full': 'host.os.full', + 'host.os.kernel': 'host.os.kernel', + 'host.os.platform': 'host.os.platform', + 'host.os.version': 'host.os.version', + }; + expect(extendMap('host', osFieldsMap)).toEqual(expected); + }); + + test('it should extend a sample hosts record', () => { + const hostMap: Record = { + 'host.id': 'host.id', + 'host.ip': 'host.ip', + 'host.name': 'host.name', + }; + const osFieldsMap: Readonly> = { + 'os.platform': 'os.platform', + 'os.full': 'os.full', + 'os.family': 'os.family', + 'os.version': 'os.version', + 'os.kernel': 'os.kernel', + }; + const expected: Record = { + 'host.id': 'host.id', + 'host.ip': 'host.ip', + 'host.name': 'host.name', + 'host.os.family': 'host.os.family', + 'host.os.full': 'host.os.full', + 'host.os.kernel': 'host.os.kernel', + 'host.os.platform': 'host.os.platform', + 'host.os.version': 'host.os.version', + }; + const output = { ...hostMap, ...extendMap('host', osFieldsMap) }; + expect(output).toEqual(expected); + }); + }); +}); diff --git a/x-pack/plugins/osquery/common/ecs/ecs_fields/extend_map.ts b/x-pack/plugins/osquery/common/ecs/ecs_fields/extend_map.ts new file mode 100644 index 0000000000000..c25979cbcdcee --- /dev/null +++ b/x-pack/plugins/osquery/common/ecs/ecs_fields/extend_map.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const extendMap = ( + path: string, + map: Readonly> +): Readonly> => + Object.entries(map).reduce>((accum, [key, value]) => { + accum[`${path}.${key}`] = `${path}.${value}`; + return accum; + }, {}); diff --git a/x-pack/plugins/osquery/common/ecs/ecs_fields/index.ts b/x-pack/plugins/osquery/common/ecs/ecs_fields/index.ts new file mode 100644 index 0000000000000..19b16bd4bc6d2 --- /dev/null +++ b/x-pack/plugins/osquery/common/ecs/ecs_fields/index.ts @@ -0,0 +1,358 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { extendMap } from './extend_map'; + +export const auditdMap: Readonly> = { + 'auditd.result': 'auditd.result', + 'auditd.session': 'auditd.session', + 'auditd.data.acct': 'auditd.data.acct', + 'auditd.data.terminal': 'auditd.data.terminal', + 'auditd.data.op': 'auditd.data.op', + 'auditd.summary.actor.primary': 'auditd.summary.actor.primary', + 'auditd.summary.actor.secondary': 'auditd.summary.actor.secondary', + 'auditd.summary.object.primary': 'auditd.summary.object.primary', + 'auditd.summary.object.secondary': 'auditd.summary.object.secondary', + 'auditd.summary.object.type': 'auditd.summary.object.type', + 'auditd.summary.how': 'auditd.summary.how', + 'auditd.summary.message_type': 'auditd.summary.message_type', + 'auditd.summary.sequence': 'auditd.summary.sequence', +}; + +export const cloudFieldsMap: Readonly> = { + 'cloud.account.id': 'cloud.account.id', + 'cloud.availability_zone': 'cloud.availability_zone', + 'cloud.instance.id': 'cloud.instance.id', + 'cloud.instance.name': 'cloud.instance.name', + 'cloud.machine.type': 'cloud.machine.type', + 'cloud.provider': 'cloud.provider', + 'cloud.region': 'cloud.region', +}; + +export const fileMap: Readonly> = { + 'file.name': 'file.name', + 'file.path': 'file.path', + 'file.target_path': 'file.target_path', + 'file.extension': 'file.extension', + 'file.type': 'file.type', + 'file.device': 'file.device', + 'file.inode': 'file.inode', + 'file.uid': 'file.uid', + 'file.owner': 'file.owner', + 'file.gid': 'file.gid', + 'file.group': 'file.group', + 'file.mode': 'file.mode', + 'file.size': 'file.size', + 'file.mtime': 'file.mtime', + 'file.ctime': 'file.ctime', +}; + +export const osFieldsMap: Readonly> = { + 'os.platform': 'os.platform', + 'os.name': 'os.name', + 'os.full': 'os.full', + 'os.family': 'os.family', + 'os.version': 'os.version', + 'os.kernel': 'os.kernel', +}; + +export const hostFieldsMap: Readonly> = { + 'host.architecture': 'host.architecture', + 'host.id': 'host.id', + 'host.ip': 'host.ip', + 'host.mac': 'host.mac', + 'host.name': 'host.name', + ...extendMap('host', osFieldsMap), +}; + +export const processFieldsMap: Readonly> = { + 'process.hash.md5': 'process.hash.md5', + 'process.hash.sha1': 'process.hash.sha1', + 'process.hash.sha256': 'process.hash.sha256', + 'process.pid': 'process.pid', + 'process.name': 'process.name', + 'process.ppid': 'process.ppid', + 'process.args': 'process.args', + 'process.entity_id': 'process.entity_id', + 'process.executable': 'process.executable', + 'process.title': 'process.title', + 'process.thread': 'process.thread', + 'process.working_directory': 'process.working_directory', +}; + +export const agentFieldsMap: Readonly> = { + 'agent.type': 'agent.type', +}; + +export const userFieldsMap: Readonly> = { + 'user.domain': 'user.domain', + 'user.id': 'user.id', + 'user.name': 'user.name', + // NOTE: This field is not tested and available from ECS. Please remove this tag once it is + 'user.full_name': 'user.full_name', + // NOTE: This field is not tested and available from ECS. Please remove this tag once it is + 'user.email': 'user.email', + // NOTE: This field is not tested and available from ECS. Please remove this tag once it is + 'user.hash': 'user.hash', + // NOTE: This field is not tested and available from ECS. Please remove this tag once it is + 'user.group': 'user.group', +}; + +export const winlogFieldsMap: Readonly> = { + 'winlog.event_id': 'winlog.event_id', +}; + +export const suricataFieldsMap: Readonly> = { + 'suricata.eve.flow_id': 'suricata.eve.flow_id', + 'suricata.eve.proto': 'suricata.eve.proto', + 'suricata.eve.alert.signature': 'suricata.eve.alert.signature', + 'suricata.eve.alert.signature_id': 'suricata.eve.alert.signature_id', +}; + +export const tlsFieldsMap: Readonly> = { + 'tls.client_certificate.fingerprint.sha1': 'tls.client_certificate.fingerprint.sha1', + 'tls.fingerprints.ja3.hash': 'tls.fingerprints.ja3.hash', + 'tls.server_certificate.fingerprint.sha1': 'tls.server_certificate.fingerprint.sha1', +}; + +export const urlFieldsMap: Readonly> = { + 'url.original': 'url.original', + 'url.domain': 'url.domain', + 'user.username': 'user.username', + 'user.password': 'user.password', +}; + +export const httpFieldsMap: Readonly> = { + 'http.version': 'http.version', + 'http.request': 'http.request', + 'http.request.method': 'http.request.method', + 'http.request.body.bytes': 'http.request.body.bytes', + 'http.request.body.content': 'http.request.body.content', + 'http.request.referrer': 'http.request.referrer', + 'http.response.status_code': 'http.response.status_code', + 'http.response.body': 'http.response.body', + 'http.response.body.bytes': 'http.response.body.bytes', + 'http.response.body.content': 'http.response.body.content', +}; + +export const zeekFieldsMap: Readonly> = { + 'zeek.session_id': 'zeek.session_id', + 'zeek.connection.local_resp': 'zeek.connection.local_resp', + 'zeek.connection.local_orig': 'zeek.connection.local_orig', + 'zeek.connection.missed_bytes': 'zeek.connection.missed_bytes', + 'zeek.connection.state': 'zeek.connection.state', + 'zeek.connection.history': 'zeek.connection.history', + 'zeek.notice.suppress_for': 'zeek.notice.suppress_for', + 'zeek.notice.msg': 'zeek.notice.msg', + 'zeek.notice.note': 'zeek.notice.note', + 'zeek.notice.sub': 'zeek.notice.sub', + 'zeek.notice.dst': 'zeek.notice.dst', + 'zeek.notice.dropped': 'zeek.notice.dropped', + 'zeek.notice.peer_descr': 'zeek.notice.peer_descr', + 'zeek.dns.AA': 'zeek.dns.AA', + 'zeek.dns.qclass_name': 'zeek.dns.qclass_name', + 'zeek.dns.RD': 'zeek.dns.RD', + 'zeek.dns.qtype_name': 'zeek.dns.qtype_name', + 'zeek.dns.qtype': 'zeek.dns.qtype', + 'zeek.dns.query': 'zeek.dns.query', + 'zeek.dns.trans_id': 'zeek.dns.trans_id', + 'zeek.dns.qclass': 'zeek.dns.qclass', + 'zeek.dns.RA': 'zeek.dns.RA', + 'zeek.dns.TC': 'zeek.dns.TC', + 'zeek.http.resp_mime_types': 'zeek.http.resp_mime_types', + 'zeek.http.trans_depth': 'zeek.http.trans_depth', + 'zeek.http.status_msg': 'zeek.http.status_msg', + 'zeek.http.resp_fuids': 'zeek.http.resp_fuids', + 'zeek.http.tags': 'zeek.http.tags', + 'zeek.files.session_ids': 'zeek.files.session_ids', + 'zeek.files.timedout': 'zeek.files.timedout', + 'zeek.files.local_orig': 'zeek.files.local_orig', + 'zeek.files.tx_host': 'zeek.files.tx_host', + 'zeek.files.source': 'zeek.files.source', + 'zeek.files.is_orig': 'zeek.files.is_orig', + 'zeek.files.overflow_bytes': 'zeek.files.overflow_bytes', + 'zeek.files.sha1': 'zeek.files.sha1', + 'zeek.files.duration': 'zeek.files.duration', + 'zeek.files.depth': 'zeek.files.depth', + 'zeek.files.analyzers': 'zeek.files.analyzers', + 'zeek.files.mime_type': 'zeek.files.mime_type', + 'zeek.files.rx_host': 'zeek.files.rx_host', + 'zeek.files.total_bytes': 'zeek.files.total_bytes', + 'zeek.files.fuid': 'zeek.files.fuid', + 'zeek.files.seen_bytes': 'zeek.files.seen_bytes', + 'zeek.files.missing_bytes': 'zeek.files.missing_bytes', + 'zeek.files.md5': 'zeek.files.md5', + 'zeek.ssl.cipher': 'zeek.ssl.cipher', + 'zeek.ssl.established': 'zeek.ssl.established', + 'zeek.ssl.resumed': 'zeek.ssl.resumed', + 'zeek.ssl.version': 'zeek.ssl.version', +}; + +export const sourceFieldsMap: Readonly> = { + 'source.bytes': 'source.bytes', + 'source.ip': 'source.ip', + 'source.packets': 'source.packets', + 'source.port': 'source.port', + 'source.domain': 'source.domain', + 'source.geo.continent_name': 'source.geo.continent_name', + 'source.geo.country_name': 'source.geo.country_name', + 'source.geo.country_iso_code': 'source.geo.country_iso_code', + 'source.geo.city_name': 'source.geo.city_name', + 'source.geo.region_iso_code': 'source.geo.region_iso_code', + 'source.geo.region_name': 'source.geo.region_name', +}; + +export const destinationFieldsMap: Readonly> = { + 'destination.bytes': 'destination.bytes', + 'destination.ip': 'destination.ip', + 'destination.packets': 'destination.packets', + 'destination.port': 'destination.port', + 'destination.domain': 'destination.domain', + 'destination.geo.continent_name': 'destination.geo.continent_name', + 'destination.geo.country_name': 'destination.geo.country_name', + 'destination.geo.country_iso_code': 'destination.geo.country_iso_code', + 'destination.geo.city_name': 'destination.geo.city_name', + 'destination.geo.region_iso_code': 'destination.geo.region_iso_code', + 'destination.geo.region_name': 'destination.geo.region_name', +}; + +export const networkFieldsMap: Readonly> = { + 'network.bytes': 'network.bytes', + 'network.community_id': 'network.community_id', + 'network.direction': 'network.direction', + 'network.packets': 'network.packets', + 'network.protocol': 'network.protocol', + 'network.transport': 'network.transport', +}; + +export const geoFieldsMap: Readonly> = { + 'geo.region_name': 'destination.geo.region_name', + 'geo.country_iso_code': 'destination.geo.country_iso_code', +}; + +export const dnsFieldsMap: Readonly> = { + 'dns.question.name': 'dns.question.name', + 'dns.question.type': 'dns.question.type', + 'dns.resolved_ip': 'dns.resolved_ip', + 'dns.response_code': 'dns.response_code', +}; + +export const endgameFieldsMap: Readonly> = { + 'endgame.exit_code': 'endgame.exit_code', + 'endgame.file_name': 'endgame.file_name', + 'endgame.file_path': 'endgame.file_path', + 'endgame.logon_type': 'endgame.logon_type', + 'endgame.parent_process_name': 'endgame.parent_process_name', + 'endgame.pid': 'endgame.pid', + 'endgame.process_name': 'endgame.process_name', + 'endgame.subject_domain_name': 'endgame.subject_domain_name', + 'endgame.subject_logon_id': 'endgame.subject_logon_id', + 'endgame.subject_user_name': 'endgame.subject_user_name', + 'endgame.target_domain_name': 'endgame.target_domain_name', + 'endgame.target_logon_id': 'endgame.target_logon_id', + 'endgame.target_user_name': 'endgame.target_user_name', +}; + +export const eventBaseFieldsMap: Readonly> = { + 'event.action': 'event.action', + 'event.category': 'event.category', + 'event.code': 'event.code', + 'event.created': 'event.created', + 'event.dataset': 'event.dataset', + 'event.duration': 'event.duration', + 'event.end': 'event.end', + 'event.hash': 'event.hash', + 'event.id': 'event.id', + 'event.kind': 'event.kind', + 'event.module': 'event.module', + 'event.original': 'event.original', + 'event.outcome': 'event.outcome', + 'event.risk_score': 'event.risk_score', + 'event.risk_score_norm': 'event.risk_score_norm', + 'event.severity': 'event.severity', + 'event.start': 'event.start', + 'event.timezone': 'event.timezone', + 'event.type': 'event.type', +}; + +export const systemFieldsMap: Readonly> = { + 'system.audit.package.arch': 'system.audit.package.arch', + 'system.audit.package.entity_id': 'system.audit.package.entity_id', + 'system.audit.package.name': 'system.audit.package.name', + 'system.audit.package.size': 'system.audit.package.size', + 'system.audit.package.summary': 'system.audit.package.summary', + 'system.audit.package.version': 'system.audit.package.version', + 'system.auth.ssh.signature': 'system.auth.ssh.signature', + 'system.auth.ssh.method': 'system.auth.ssh.method', +}; + +export const signalFieldsMap: Readonly> = { + 'signal.original_time': 'signal.original_time', + 'signal.rule.id': 'signal.rule.id', + 'signal.rule.saved_id': 'signal.rule.saved_id', + 'signal.rule.timeline_id': 'signal.rule.timeline_id', + 'signal.rule.timeline_title': 'signal.rule.timeline_title', + 'signal.rule.output_index': 'signal.rule.output_index', + 'signal.rule.from': 'signal.rule.from', + 'signal.rule.index': 'signal.rule.index', + 'signal.rule.language': 'signal.rule.language', + 'signal.rule.query': 'signal.rule.query', + 'signal.rule.to': 'signal.rule.to', + 'signal.rule.filters': 'signal.rule.filters', + 'signal.rule.rule_id': 'signal.rule.rule_id', + 'signal.rule.false_positives': 'signal.rule.false_positives', + 'signal.rule.max_signals': 'signal.rule.max_signals', + 'signal.rule.risk_score': 'signal.rule.risk_score', + 'signal.rule.description': 'signal.rule.description', + 'signal.rule.name': 'signal.rule.name', + 'signal.rule.immutable': 'signal.rule.immutable', + 'signal.rule.references': 'signal.rule.references', + 'signal.rule.severity': 'signal.rule.severity', + 'signal.rule.tags': 'signal.rule.tags', + 'signal.rule.threat': 'signal.rule.threat', + 'signal.rule.type': 'signal.rule.type', + 'signal.rule.size': 'signal.rule.size', + 'signal.rule.enabled': 'signal.rule.enabled', + 'signal.rule.created_at': 'signal.rule.created_at', + 'signal.rule.updated_at': 'signal.rule.updated_at', + 'signal.rule.created_by': 'signal.rule.created_by', + 'signal.rule.updated_by': 'signal.rule.updated_by', + 'signal.rule.version': 'signal.rule.version', + 'signal.rule.note': 'signal.rule.note', + 'signal.rule.threshold': 'signal.rule.threshold', + 'signal.rule.exceptions_list': 'signal.rule.exceptions_list', +}; + +export const ruleFieldsMap: Readonly> = { + 'rule.reference': 'rule.reference', +}; + +export const eventFieldsMap: Readonly> = { + timestamp: '@timestamp', + '@timestamp': '@timestamp', + message: 'message', + ...{ ...agentFieldsMap }, + ...{ ...auditdMap }, + ...{ ...destinationFieldsMap }, + ...{ ...dnsFieldsMap }, + ...{ ...endgameFieldsMap }, + ...{ ...eventBaseFieldsMap }, + ...{ ...fileMap }, + ...{ ...geoFieldsMap }, + ...{ ...hostFieldsMap }, + ...{ ...networkFieldsMap }, + ...{ ...ruleFieldsMap }, + ...{ ...signalFieldsMap }, + ...{ ...sourceFieldsMap }, + ...{ ...suricataFieldsMap }, + ...{ ...systemFieldsMap }, + ...{ ...tlsFieldsMap }, + ...{ ...zeekFieldsMap }, + ...{ ...httpFieldsMap }, + ...{ ...userFieldsMap }, + ...{ ...winlogFieldsMap }, + ...{ ...processFieldsMap }, +}; diff --git a/x-pack/plugins/osquery/common/ecs/endgame/index.ts b/x-pack/plugins/osquery/common/ecs/endgame/index.ts new file mode 100644 index 0000000000000..d2fc5d61527a5 --- /dev/null +++ b/x-pack/plugins/osquery/common/ecs/endgame/index.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 EndgameEcs { + exit_code?: number[]; + file_name?: string[]; + file_path?: string[]; + logon_type?: number[]; + parent_process_name?: string[]; + pid?: number[]; + process_name?: string[]; + subject_domain_name?: string[]; + subject_logon_id?: string[]; + subject_user_name?: string[]; + target_domain_name?: string[]; + target_logon_id?: string[]; + target_user_name?: string[]; +} diff --git a/x-pack/plugins/osquery/common/ecs/event/index.ts b/x-pack/plugins/osquery/common/ecs/event/index.ts new file mode 100644 index 0000000000000..c3b7b1d0b8436 --- /dev/null +++ b/x-pack/plugins/osquery/common/ecs/event/index.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. + */ + +export interface EventEcs { + action?: string[]; + category?: string[]; + code?: string[]; + created?: string[]; + dataset?: string[]; + duration?: number[]; + end?: string[]; + hash?: string[]; + id?: string[]; + kind?: string[]; + module?: string[]; + original?: string[]; + outcome?: string[]; + risk_score?: number[]; + risk_score_norm?: number[]; + severity?: number[]; + start?: string[]; + timezone?: string[]; + type?: string[]; +} diff --git a/x-pack/plugins/osquery/common/ecs/file/index.ts b/x-pack/plugins/osquery/common/ecs/file/index.ts new file mode 100644 index 0000000000000..b01e9514bf425 --- /dev/null +++ b/x-pack/plugins/osquery/common/ecs/file/index.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. + */ + +export interface CodeSignature { + subject_name: string[]; + trusted: string[]; +} +export interface Ext { + code_signature: CodeSignature[] | CodeSignature; +} +export interface Hash { + sha256: string[]; +} + +export interface FileEcs { + name?: string[]; + path?: string[]; + target_path?: string[]; + extension?: string[]; + Ext?: Ext; + type?: string[]; + device?: string[]; + inode?: string[]; + uid?: string[]; + owner?: string[]; + gid?: string[]; + group?: string[]; + mode?: string[]; + size?: number[]; + mtime?: string[]; + ctime?: string[]; + hash?: Hash; +} diff --git a/x-pack/plugins/osquery/common/ecs/geo/index.ts b/x-pack/plugins/osquery/common/ecs/geo/index.ts new file mode 100644 index 0000000000000..4a4c76adb097b --- /dev/null +++ b/x-pack/plugins/osquery/common/ecs/geo/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. + */ + +export interface GeoEcs { + city_name?: string[]; + continent_name?: string[]; + country_iso_code?: string[]; + country_name?: string[]; + location?: Location; + region_iso_code?: string[]; + region_name?: string[]; +} + +export interface Location { + lon?: number[]; + lat?: number[]; +} diff --git a/x-pack/plugins/osquery/common/ecs/host/index.ts b/x-pack/plugins/osquery/common/ecs/host/index.ts new file mode 100644 index 0000000000000..27cbe433f9bf7 --- /dev/null +++ b/x-pack/plugins/osquery/common/ecs/host/index.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. + */ + +export interface HostEcs { + architecture?: string[]; + id?: string[]; + ip?: string[]; + mac?: string[]; + name?: string[]; + os?: OsEcs; + type?: string[]; +} + +export interface OsEcs { + platform?: string[]; + name?: string[]; + full?: string[]; + family?: string[]; + version?: string[]; + kernel?: string[]; +} diff --git a/x-pack/plugins/osquery/common/ecs/http/index.ts b/x-pack/plugins/osquery/common/ecs/http/index.ts new file mode 100644 index 0000000000000..c5c5d1e140d0a --- /dev/null +++ b/x-pack/plugins/osquery/common/ecs/http/index.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface HttpEcs { + version?: string[]; + request?: HttpRequestData; + response?: HttpResponseData; +} + +export interface HttpRequestData { + method?: string[]; + body?: HttpBodyData; + referrer?: string[]; + bytes?: number[]; +} + +export interface HttpBodyData { + content?: string[]; + bytes?: number[]; +} + +export interface HttpResponseData { + status_code?: number[]; + body?: HttpBodyData; + bytes?: number[]; +} diff --git a/x-pack/plugins/osquery/common/ecs/index.ts b/x-pack/plugins/osquery/common/ecs/index.ts new file mode 100644 index 0000000000000..b8190463f5da5 --- /dev/null +++ b/x-pack/plugins/osquery/common/ecs/index.ts @@ -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 { AgentEcs } from './agent'; +import { AuditdEcs } from './auditd'; +import { DestinationEcs } from './destination'; +import { DnsEcs } from './dns'; +import { EndgameEcs } from './endgame'; +import { EventEcs } from './event'; +import { FileEcs } from './file'; +import { GeoEcs } from './geo'; +import { HostEcs } from './host'; +import { NetworkEcs } from './network'; +import { RuleEcs } from './rule'; +import { SignalEcs } from './signal'; +import { SourceEcs } from './source'; +import { SuricataEcs } from './suricata'; +import { TlsEcs } from './tls'; +import { ZeekEcs } from './zeek'; +import { HttpEcs } from './http'; +import { UrlEcs } from './url'; +import { UserEcs } from './user'; +import { WinlogEcs } from './winlog'; +import { ProcessEcs } from './process'; +import { SystemEcs } from './system'; + +export interface Ecs { + _id: string; + _index?: string; + agent?: AgentEcs; + auditd?: AuditdEcs; + destination?: DestinationEcs; + dns?: DnsEcs; + endgame?: EndgameEcs; + event?: EventEcs; + geo?: GeoEcs; + host?: HostEcs; + network?: NetworkEcs; + rule?: RuleEcs; + signal?: SignalEcs; + source?: SourceEcs; + suricata?: SuricataEcs; + tls?: TlsEcs; + zeek?: ZeekEcs; + http?: HttpEcs; + url?: UrlEcs; + timestamp?: string; + message?: string[]; + user?: UserEcs; + winlog?: WinlogEcs; + process?: ProcessEcs; + file?: FileEcs; + system?: SystemEcs; +} diff --git a/x-pack/plugins/osquery/common/ecs/network/index.ts b/x-pack/plugins/osquery/common/ecs/network/index.ts new file mode 100644 index 0000000000000..18f7583d12231 --- /dev/null +++ b/x-pack/plugins/osquery/common/ecs/network/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface NetworkEcs { + bytes?: number[]; + community_id?: string[]; + direction?: string[]; + packets?: number[]; + protocol?: string[]; + transport?: string[]; +} diff --git a/x-pack/plugins/osquery/common/ecs/process/index.ts b/x-pack/plugins/osquery/common/ecs/process/index.ts new file mode 100644 index 0000000000000..451f1455f55d4 --- /dev/null +++ b/x-pack/plugins/osquery/common/ecs/process/index.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface ProcessEcs { + entity_id?: string[]; + hash?: ProcessHashData; + pid?: number[]; + name?: string[]; + ppid?: number[]; + args?: string[]; + executable?: string[]; + title?: string[]; + thread?: Thread; + working_directory?: string[]; +} + +export interface ProcessHashData { + md5?: string[]; + sha1?: string[]; + sha256?: string[]; +} + +export interface Thread { + id?: number[]; + start?: string[]; +} diff --git a/x-pack/plugins/osquery/common/ecs/rule/index.ts b/x-pack/plugins/osquery/common/ecs/rule/index.ts new file mode 100644 index 0000000000000..47d1323371941 --- /dev/null +++ b/x-pack/plugins/osquery/common/ecs/rule/index.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface RuleEcs { + id?: string[]; + rule_id?: string[]; + name?: string[]; + false_positives: string[]; + saved_id?: string[]; + timeline_id?: string[]; + timeline_title?: string[]; + max_signals?: number[]; + risk_score?: string[]; + output_index?: string[]; + description?: string[]; + from?: string[]; + immutable?: boolean[]; + index?: string[]; + interval?: string[]; + language?: string[]; + query?: string[]; + references?: string[]; + severity?: string[]; + tags?: string[]; + threat?: unknown; + threshold?: { + field: string; + value: number; + }; + type?: string[]; + size?: string[]; + to?: string[]; + enabled?: boolean[]; + filters?: unknown; + created_at?: string[]; + updated_at?: string[]; + created_by?: string[]; + updated_by?: string[]; + version?: string[]; + note?: string[]; + building_block_type?: string[]; +} diff --git a/x-pack/plugins/osquery/common/ecs/signal/index.ts b/x-pack/plugins/osquery/common/ecs/signal/index.ts new file mode 100644 index 0000000000000..6482b892bc18d --- /dev/null +++ b/x-pack/plugins/osquery/common/ecs/signal/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 { RuleEcs } from '../rule'; + +export interface SignalEcs { + rule?: RuleEcs; + original_time?: string[]; + status?: string[]; + group?: { + id?: string[]; + }; +} diff --git a/x-pack/plugins/osquery/common/ecs/source/index.ts b/x-pack/plugins/osquery/common/ecs/source/index.ts new file mode 100644 index 0000000000000..2c8618f4edcd0 --- /dev/null +++ b/x-pack/plugins/osquery/common/ecs/source/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 { GeoEcs } from '../geo'; + +export interface SourceEcs { + bytes?: number[]; + ip?: string[]; + port?: number[]; + domain?: string[]; + geo?: GeoEcs; + packets?: number[]; +} diff --git a/x-pack/plugins/osquery/common/ecs/suricata/index.ts b/x-pack/plugins/osquery/common/ecs/suricata/index.ts new file mode 100644 index 0000000000000..0ef253ada2620 --- /dev/null +++ b/x-pack/plugins/osquery/common/ecs/suricata/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. + */ + +export interface SuricataEcs { + eve?: SuricataEveData; +} + +export interface SuricataEveData { + alert?: SuricataAlertData; + flow_id?: number[]; + proto?: string[]; +} + +export interface SuricataAlertData { + signature?: string[]; + signature_id?: number[]; +} diff --git a/x-pack/plugins/osquery/common/ecs/system/index.ts b/x-pack/plugins/osquery/common/ecs/system/index.ts new file mode 100644 index 0000000000000..641a10209c150 --- /dev/null +++ b/x-pack/plugins/osquery/common/ecs/system/index.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. + */ + +export interface SystemEcs { + audit?: AuditEcs; + auth?: AuthEcs; +} + +export interface AuditEcs { + package?: PackageEcs; +} + +export interface PackageEcs { + arch?: string[]; + entity_id?: string[]; + name?: string[]; + size?: number[]; + summary?: string[]; + version?: string[]; +} + +export interface AuthEcs { + ssh?: SshEcs; +} + +export interface SshEcs { + method?: string[]; + signature?: string[]; +} diff --git a/x-pack/plugins/osquery/common/ecs/tls/index.ts b/x-pack/plugins/osquery/common/ecs/tls/index.ts new file mode 100644 index 0000000000000..1533d46417d0a --- /dev/null +++ b/x-pack/plugins/osquery/common/ecs/tls/index.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 TlsEcs { + client_certificate?: TlsClientCertificateData; + fingerprints?: TlsFingerprintsData; + server_certificate?: TlsServerCertificateData; +} + +export interface TlsClientCertificateData { + fingerprint?: FingerprintData; +} + +export interface FingerprintData { + sha1?: string[]; +} + +export interface TlsFingerprintsData { + ja3?: TlsJa3Data; +} + +export interface TlsJa3Data { + hash?: string[]; +} + +export interface TlsServerCertificateData { + fingerprint?: FingerprintData; +} diff --git a/x-pack/plugins/osquery/common/ecs/url/index.ts b/x-pack/plugins/osquery/common/ecs/url/index.ts new file mode 100644 index 0000000000000..91d7958c813a3 --- /dev/null +++ b/x-pack/plugins/osquery/common/ecs/url/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. + */ + +export interface UrlEcs { + domain?: string[]; + original?: string[]; + username?: string[]; + password?: string[]; +} diff --git a/x-pack/plugins/osquery/common/ecs/user/index.ts b/x-pack/plugins/osquery/common/ecs/user/index.ts new file mode 100644 index 0000000000000..35de2e0459ceb --- /dev/null +++ b/x-pack/plugins/osquery/common/ecs/user/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 interface UserEcs { + domain?: string[]; + id?: string[]; + name?: string[]; + full_name?: string[]; + email?: string[]; + hash?: string[]; + group?: string[]; +} diff --git a/x-pack/plugins/osquery/common/ecs/winlog/index.ts b/x-pack/plugins/osquery/common/ecs/winlog/index.ts new file mode 100644 index 0000000000000..a449fb9130e6f --- /dev/null +++ b/x-pack/plugins/osquery/common/ecs/winlog/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 interface WinlogEcs { + event_id?: number[]; +} diff --git a/x-pack/plugins/osquery/common/ecs/zeek/index.ts b/x-pack/plugins/osquery/common/ecs/zeek/index.ts new file mode 100644 index 0000000000000..2563612f09bfb --- /dev/null +++ b/x-pack/plugins/osquery/common/ecs/zeek/index.ts @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 ZeekEcs { + session_id?: string[]; + connection?: ZeekConnectionData; + notice?: ZeekNoticeData; + dns?: ZeekDnsData; + http?: ZeekHttpData; + files?: ZeekFileData; + ssl?: ZeekSslData; +} + +export interface ZeekConnectionData { + local_resp?: boolean[]; + local_orig?: boolean[]; + missed_bytes?: number[]; + state?: string[]; + history?: string[]; +} + +export interface ZeekNoticeData { + suppress_for?: number[]; + msg?: string[]; + note?: string[]; + sub?: string[]; + dst?: string[]; + dropped?: boolean[]; + peer_descr?: string[]; +} + +export interface ZeekDnsData { + AA?: boolean[]; + qclass_name?: string[]; + RD?: boolean[]; + qtype_name?: string[]; + rejected?: boolean[]; + qtype?: string[]; + query?: string[]; + trans_id?: number[]; + qclass?: string[]; + RA?: boolean[]; + TC?: boolean[]; +} + +export interface ZeekHttpData { + resp_mime_types?: string[]; + trans_depth?: string[]; + status_msg?: string[]; + resp_fuids?: string[]; + tags?: string[]; +} + +export interface ZeekFileData { + session_ids?: string[]; + timedout?: boolean[]; + local_orig?: boolean[]; + tx_host?: string[]; + source?: string[]; + is_orig?: boolean[]; + overflow_bytes?: number[]; + sha1?: string[]; + duration?: number[]; + depth?: number[]; + analyzers?: string[]; + mime_type?: string[]; + rx_host?: string[]; + total_bytes?: number[]; + fuid?: string[]; + seen_bytes?: number[]; + missing_bytes?: number[]; + md5?: string[]; +} + +export interface ZeekSslData { + cipher?: string[]; + established?: boolean[]; + resumed?: boolean[]; + version?: string[]; +} diff --git a/x-pack/plugins/osquery/common/index.ts b/x-pack/plugins/osquery/common/index.ts new file mode 100644 index 0000000000000..e4bbf4781e881 --- /dev/null +++ b/x-pack/plugins/osquery/common/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './constants'; + +export const PLUGIN_ID = 'osquery'; +export const PLUGIN_NAME = 'osquery'; diff --git a/x-pack/plugins/osquery/common/search_strategy/common/index.ts b/x-pack/plugins/osquery/common/search_strategy/common/index.ts new file mode 100644 index 0000000000000..0c1f13dac2e69 --- /dev/null +++ b/x-pack/plugins/osquery/common/search_strategy/common/index.ts @@ -0,0 +1,125 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { IEsSearchResponse } from '../../../../../../src/plugins/data/common'; + +export type Maybe = T | null; + +export type SearchHit = IEsSearchResponse['rawResponse']['hits']['hits'][0]; + +export interface TotalValue { + value: number; + relation: string; +} + +export interface Inspect { + dsl: string[]; +} + +export interface PageInfoPaginated { + activePage: number; + fakeTotalCount: number; + showMorePagesIndicator: boolean; +} + +export interface CursorType { + value?: Maybe; + tiebreaker?: Maybe; +} + +export enum Direction { + asc = 'asc', + desc = 'desc', +} + +export interface SortField { + field: Field; + direction: Direction; +} + +export interface TimerangeInput { + /** The interval string to use for last bucket. The format is '{value}{unit}'. For example '5m' would return the metrics for the last 5 minutes of the timespan. */ + interval: string; + /** The end of the timerange */ + to: string; + /** The beginning of the timerange */ + from: string; +} + +export interface PaginationInput { + /** The limit parameter allows you to configure the maximum amount of items to be returned */ + limit: number; + /** The cursor parameter defines the next result you want to fetch */ + cursor?: Maybe; + /** The tiebreaker parameter allow to be more precise to fetch the next item */ + tiebreaker?: Maybe; +} + +export interface PaginationInputPaginated { + /** The activePage parameter defines the page of results you want to fetch */ + activePage: number; + /** The cursorStart parameter defines the start of the results to be displayed */ + cursorStart: number; + /** The fakePossibleCount parameter determines the total count in order to show 5 additional pages */ + fakePossibleCount: number; + /** The querySize parameter is the number of items to be returned */ + querySize: number; +} + +export interface DocValueFields { + field: string; + format?: string | null; +} + +export interface Explanation { + value: number; + description: string; + details: Explanation[]; +} + +export interface ShardsResponse { + total: number; + successful: number; + failed: number; + skipped: number; +} + +export interface TotalHit { + value: number; + relation: string; +} + +export interface Hit { + _index: string; + _type: string; + _id: string; + _score: number | null; +} + +export interface Hits { + hits: { + total: T; + max_score: number | null; + hits: U[]; + }; +} + +export interface GenericBuckets { + key: string; + doc_count: number; +} + +export type StringOrNumber = string | number; + +export interface TimerangeFilter { + range: { + [timestamp: string]: { + gte: string; + lte: string; + format: string; + }; + }; +} diff --git a/x-pack/plugins/osquery/common/search_strategy/index.ts b/x-pack/plugins/osquery/common/search_strategy/index.ts new file mode 100644 index 0000000000000..ff9a8d1aa64c9 --- /dev/null +++ b/x-pack/plugins/osquery/common/search_strategy/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 './common'; +export * from './osquery'; diff --git a/x-pack/plugins/osquery/common/search_strategy/osquery/actions/index.ts b/x-pack/plugins/osquery/common/search_strategy/osquery/actions/index.ts new file mode 100644 index 0000000000000..076fa02747573 --- /dev/null +++ b/x-pack/plugins/osquery/common/search_strategy/osquery/actions/index.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 { SearchResponse } from 'elasticsearch'; +import { IEsSearchResponse } from '../../../../../../../src/plugins/data/common'; + +import { Inspect, Maybe, PageInfoPaginated } from '../../common'; +import { RequestOptions, RequestOptionsPaginated } from '../..'; + +export type ActionEdges = SearchResponse['hits']['hits']; + +export type ActionResultEdges = SearchResponse['hits']['hits']; +export interface ActionsStrategyResponse extends IEsSearchResponse { + edges: ActionEdges; + totalCount: number; + pageInfo: PageInfoPaginated; + inspect?: Maybe; +} + +export type ActionsRequestOptions = RequestOptionsPaginated<{}>; + +export interface ActionDetailsStrategyResponse extends IEsSearchResponse { + actionDetails: Record; + inspect?: Maybe; +} + +export interface ActionDetailsRequestOptions extends RequestOptions { + actionId: string; +} + +export interface ActionResultsStrategyResponse extends IEsSearchResponse { + edges: ActionResultEdges; + totalCount: number; + pageInfo: PageInfoPaginated; + inspect?: Maybe; +} + +export interface ActionResultsRequestOptions extends RequestOptionsPaginated { + actionId: string; +} diff --git a/x-pack/plugins/osquery/common/search_strategy/osquery/agents/index.ts b/x-pack/plugins/osquery/common/search_strategy/osquery/agents/index.ts new file mode 100644 index 0000000000000..64a570ef5525b --- /dev/null +++ b/x-pack/plugins/osquery/common/search_strategy/osquery/agents/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 { IEsSearchResponse } from '../../../../../../../src/plugins/data/common'; + +import { Inspect, Maybe, PageInfoPaginated } from '../../common'; +import { RequestOptionsPaginated } from '../..'; +import { Agent } from '../../../shared_imports'; + +export interface AgentsStrategyResponse extends IEsSearchResponse { + edges: Agent[]; + totalCount: number; + pageInfo: PageInfoPaginated; + inspect?: Maybe; +} + +export type AgentsRequestOptions = RequestOptionsPaginated<{}>; diff --git a/x-pack/plugins/osquery/common/search_strategy/osquery/common/index.ts b/x-pack/plugins/osquery/common/search_strategy/osquery/common/index.ts new file mode 100644 index 0000000000000..fc58184f40afe --- /dev/null +++ b/x-pack/plugins/osquery/common/search_strategy/osquery/common/index.ts @@ -0,0 +1,112 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { CloudEcs } from '../../../ecs/cloud'; +import { HostEcs, OsEcs } from '../../../ecs/host'; +import { Hit, Hits, Maybe, SearchHit, StringOrNumber, TotalValue } from '../../common'; + +export enum HostPolicyResponseActionStatus { + success = 'success', + failure = 'failure', + warning = 'warning', +} + +export enum HostsFields { + lastSeen = 'lastSeen', + hostName = 'hostName', +} + +export interface EndpointFields { + endpointPolicy?: Maybe; + sensorVersion?: Maybe; + policyStatus?: Maybe; +} + +export interface HostItem { + _id?: Maybe; + cloud?: Maybe; + endpoint?: Maybe; + host?: Maybe; + lastSeen?: Maybe; +} + +export interface HostValue { + value: number; + value_as_string: string; +} + +export interface HostBucketItem { + key: string; + doc_count: number; + timestamp: HostValue; +} + +export interface HostBuckets { + buckets: HostBucketItem[]; +} + +export interface HostOsHitsItem { + hits: { + total: TotalValue | number; + max_score: number | null; + hits: Array<{ + _source: { host: { os: Maybe } }; + sort?: [number]; + _index?: string; + _type?: string; + _id?: string; + _score?: number | null; + }>; + }; +} + +export interface HostAggEsItem { + cloud_instance_id?: HostBuckets; + cloud_machine_type?: HostBuckets; + cloud_provider?: HostBuckets; + cloud_region?: HostBuckets; + firstSeen?: HostValue; + host_architecture?: HostBuckets; + host_id?: HostBuckets; + host_ip?: HostBuckets; + host_mac?: HostBuckets; + host_name?: HostBuckets; + host_os_name?: HostBuckets; + host_os_version?: HostBuckets; + host_type?: HostBuckets; + key?: string; + lastSeen?: HostValue; + os?: HostOsHitsItem; +} + +export interface HostEsData extends SearchHit { + sort: string[]; + aggregations: { + host_count: { + value: number; + }; + host_data: { + buckets: HostAggEsItem[]; + }; + }; +} + +export interface HostAggEsData extends SearchHit { + sort: string[]; + aggregations: HostAggEsItem; +} + +export interface HostHit extends Hit { + _source: { + '@timestamp'?: string; + host: HostEcs; + }; + cursor?: string; + firstSeen?: string; + sort?: StringOrNumber[]; +} + +export type HostHits = Hits; diff --git a/x-pack/plugins/osquery/common/search_strategy/osquery/index.ts b/x-pack/plugins/osquery/common/search_strategy/osquery/index.ts new file mode 100644 index 0000000000000..70882ffcc2e5c --- /dev/null +++ b/x-pack/plugins/osquery/common/search_strategy/osquery/index.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 { IEsSearchRequest } from '../../../../../../src/plugins/data/common'; +import { ESQuery } from '../../typed_json'; +import { + ActionsStrategyResponse, + ActionsRequestOptions, + ActionDetailsStrategyResponse, + ActionDetailsRequestOptions, + ActionResultsStrategyResponse, + ActionResultsRequestOptions, +} from './actions'; +import { AgentsStrategyResponse, AgentsRequestOptions } from './agents'; +import { ResultsStrategyResponse, ResultsRequestOptions } from './results'; + +import { DocValueFields, SortField, PaginationInputPaginated } from '../common'; + +export * from './actions'; +export * from './agents'; +export * from './results'; + +export enum OsqueryQueries { + actions = 'actions', + actionDetails = 'actionDetails', + actionResults = 'actionResults', + agents = 'agents', + results = 'results', +} + +export type FactoryQueryTypes = OsqueryQueries; + +export interface RequestBasicOptions extends IEsSearchRequest { + filterQuery: ESQuery | string | undefined; + docValueFields?: DocValueFields[]; + factoryQueryType?: FactoryQueryTypes; +} + +/** A mapping of semantic fields to their document counterparts */ + +export type RequestOptions = RequestBasicOptions; + +export interface RequestOptionsPaginated extends RequestBasicOptions { + pagination: PaginationInputPaginated; + sort: SortField; +} + +export type StrategyResponseType = T extends OsqueryQueries.actions + ? ActionsStrategyResponse + : T extends OsqueryQueries.actionDetails + ? ActionDetailsStrategyResponse + : T extends OsqueryQueries.actionResults + ? ActionResultsStrategyResponse + : T extends OsqueryQueries.agents + ? AgentsStrategyResponse + : T extends OsqueryQueries.results + ? ResultsStrategyResponse + : never; + +export type StrategyRequestType = T extends OsqueryQueries.actions + ? ActionsRequestOptions + : T extends OsqueryQueries.actionDetails + ? ActionDetailsRequestOptions + : T extends OsqueryQueries.actionResults + ? ActionResultsRequestOptions + : T extends OsqueryQueries.agents + ? AgentsRequestOptions + : T extends OsqueryQueries.results + ? ResultsRequestOptions + : never; diff --git a/x-pack/plugins/osquery/common/search_strategy/osquery/results/index.ts b/x-pack/plugins/osquery/common/search_strategy/osquery/results/index.ts new file mode 100644 index 0000000000000..65df2591338e4 --- /dev/null +++ b/x-pack/plugins/osquery/common/search_strategy/osquery/results/index.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 { SearchResponse } from 'elasticsearch'; +import { IEsSearchResponse } from '../../../../../../../src/plugins/data/common'; + +import { Inspect, Maybe, PageInfoPaginated } from '../../common'; +import { RequestOptionsPaginated } from '../..'; + +export type ResultEdges = SearchResponse['hits']['hits']; + +export interface ResultsStrategyResponse extends IEsSearchResponse { + edges: ResultEdges; + totalCount: number; + pageInfo: PageInfoPaginated; + inspect?: Maybe; +} + +export interface ResultsRequestOptions extends RequestOptionsPaginated<{}> { + actionId: string; +} diff --git a/x-pack/plugins/osquery/common/shared_imports.ts b/x-pack/plugins/osquery/common/shared_imports.ts new file mode 100644 index 0000000000000..58133db6aa1b0 --- /dev/null +++ b/x-pack/plugins/osquery/common/shared_imports.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 { Agent } from '../../fleet/common'; diff --git a/x-pack/plugins/osquery/common/typed_json.ts b/x-pack/plugins/osquery/common/typed_json.ts new file mode 100644 index 0000000000000..0d6e3877eae55 --- /dev/null +++ b/x-pack/plugins/osquery/common/typed_json.ts @@ -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 { DslQuery, Filter } from 'src/plugins/data/common'; + +import { JsonObject } from '../../../../src/plugins/kibana_utils/common'; + +export type ESQuery = + | ESRangeQuery + | ESQueryStringQuery + | ESMatchQuery + | ESTermQuery + | ESBoolQuery + | JsonObject; + +export interface ESRangeQuery { + range: { + [name: string]: { + gte: number; + lte: number; + format: string; + }; + }; +} + +export interface ESMatchQuery { + match: { + [name: string]: { + query: string; + operator: string; + zero_terms_query: string; + }; + }; +} + +export interface ESQueryStringQuery { + query_string: { + query: string; + analyze_wildcard: boolean; + }; +} + +export interface ESTermQuery { + term: Record; +} + +export interface ESBoolQuery { + bool: { + must: DslQuery[]; + filter: Filter[]; + should: never[]; + must_not: Filter[]; + }; +} diff --git a/x-pack/plugins/osquery/common/utility_types.ts b/x-pack/plugins/osquery/common/utility_types.ts new file mode 100644 index 0000000000000..4a7bd02d0442b --- /dev/null +++ b/x-pack/plugins/osquery/common/utility_types.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 * as runtimeTypes from 'io-ts'; +import { ReactNode } from 'react'; + +// This type is for typing EuiDescriptionList +export interface DescriptionList { + title: NonNullable; + description: NonNullable; +} + +export const unionWithNullType = (type: T) => + runtimeTypes.union([type, runtimeTypes.null]); + +export const stringEnum = (enumObj: T, enumName = 'enum') => + new runtimeTypes.Type( + enumName, + (u): u is T[keyof T] => Object.values(enumObj).includes(u), + (u, c) => + Object.values(enumObj).includes(u) + ? runtimeTypes.success(u as T[keyof T]) + : runtimeTypes.failure(u, c), + (a) => (a as unknown) as string + ); + +/** + * Unreachable Assertion helper for scenarios like exhaustive switches. + * For references see: https://stackoverflow.com/questions/39419170/how-do-i-check-that-a-switch-block-is-exhaustive-in-typescript + * This "x" should _always_ be a type of "never" and not change to "unknown" or any other type. See above link or the generic + * concept of exhaustive checks in switch blocks. + * + * Optionally you can avoid the use of this by using early returns and TypeScript will clear your type checking without complaints + * but there are situations and times where this function might still be needed. + * @param x Unreachable field + * @param message Message of error thrown + */ +export const assertUnreachable = ( + x: never, // This should always be a type of "never" + message = 'Unknown Field in switch statement' +): never => { + throw new Error(`${message}: ${x}`); +}; diff --git a/x-pack/plugins/osquery/common/utils/build_query/filters.ts b/x-pack/plugins/osquery/common/utils/build_query/filters.ts new file mode 100644 index 0000000000000..bde03be3f5edc --- /dev/null +++ b/x-pack/plugins/osquery/common/utils/build_query/filters.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 { isEmpty, isString } from 'lodash/fp'; + +import { ESQuery } from '../../../common/typed_json'; + +export const createQueryFilterClauses = (filterQuery: ESQuery | string | undefined) => + !isEmpty(filterQuery) ? [isString(filterQuery) ? JSON.parse(filterQuery) : filterQuery] : []; diff --git a/x-pack/plugins/osquery/common/utils/build_query/index.ts b/x-pack/plugins/osquery/common/utils/build_query/index.ts new file mode 100644 index 0000000000000..05606d556528c --- /dev/null +++ b/x-pack/plugins/osquery/common/utils/build_query/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 './filters'; + +export const inspectStringifyObject = (obj: unknown) => { + try { + return JSON.stringify(obj, null, 2); + } catch { + return 'Sorry about that, something went wrong.'; + } +}; diff --git a/x-pack/plugins/osquery/jest.config.js b/x-pack/plugins/osquery/jest.config.js new file mode 100644 index 0000000000000..8132491df8534 --- /dev/null +++ b/x-pack/plugins/osquery/jest.config.js @@ -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. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/x-pack/plugins/osquery'], +}; diff --git a/x-pack/plugins/osquery/kibana.json b/x-pack/plugins/osquery/kibana.json new file mode 100644 index 0000000000000..f6e90b9460506 --- /dev/null +++ b/x-pack/plugins/osquery/kibana.json @@ -0,0 +1,29 @@ +{ + "configPath": [ + "xpack", + "osquery" + ], + "extraPublicDirs": [ + "common" + ], + "id": "osquery", + "kibanaVersion": "kibana", + "optionalPlugins": [ + "home" + ], + "requiredBundles": [ + "esUiShared", + "kibanaUtils", + "kibanaReact", + "kibanaUtils" + ], + "requiredPlugins": [ + "data", + "dataEnhanced", + "fleet", + "navigation" + ], + "server": true, + "ui": true, + "version": "8.0.0" +} diff --git a/x-pack/plugins/osquery/public/action_results/action_results_table.tsx b/x-pack/plugins/osquery/public/action_results/action_results_table.tsx new file mode 100644 index 0000000000000..68424d848a9c7 --- /dev/null +++ b/x-pack/plugins/osquery/public/action_results/action_results_table.tsx @@ -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 { isEmpty, isEqual, keys, map } from 'lodash/fp'; +import { EuiDataGrid, EuiDataGridProps, EuiDataGridColumn, EuiDataGridSorting } from '@elastic/eui'; +import React, { createContext, useEffect, useState, useCallback, useContext, useMemo } from 'react'; + +import { useAllResults } from './use_action_results'; +import { Direction, ResultEdges } from '../../common/search_strategy'; + +const DataContext = createContext([]); + +interface ActionResultsTableProps { + actionId: string; +} + +const ActionResultsTableComponent: React.FC = ({ actionId }) => { + const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 50 }); + const onChangeItemsPerPage = useCallback( + (pageSize) => + setPagination((currentPagination) => ({ + ...currentPagination, + pageSize, + pageIndex: 0, + })), + [setPagination] + ); + const onChangePage = useCallback( + (pageIndex) => setPagination((currentPagination) => ({ ...currentPagination, pageIndex })), + [setPagination] + ); + + const [columns, setColumns] = useState([]); + + // ** Sorting config + const [sortingColumns, setSortingColumns] = useState([]); + + const [, { results, totalCount }] = useAllResults({ + actionId, + activePage: pagination.pageIndex, + limit: pagination.pageSize, + direction: Direction.asc, + sortField: '@timestamp', + }); + + // Column visibility + const [visibleColumns, setVisibleColumns] = useState([]); // initialize to the full set of columns + + const columnVisibility = useMemo(() => ({ visibleColumns, setVisibleColumns }), [ + visibleColumns, + setVisibleColumns, + ]); + + const renderCellValue: EuiDataGridProps['renderCellValue'] = useMemo( + () => ({ rowIndex, columnId, setCellProps }) => { + // eslint-disable-next-line react-hooks/rules-of-hooks + const data = useContext(DataContext); + const value = data[rowIndex].fields[columnId]; + + return !isEmpty(value) ? value : '-'; + }, + [] + ); + + const tableSorting: EuiDataGridSorting = useMemo( + () => ({ columns: sortingColumns, onSort: setSortingColumns }), + [sortingColumns] + ); + + const tablePagination = useMemo( + () => ({ + ...pagination, + pageSizeOptions: [10, 50, 100], + onChangeItemsPerPage, + onChangePage, + }), + [onChangeItemsPerPage, onChangePage, pagination] + ); + + useEffect(() => { + const newColumns = keys(results[0]?.fields) + .sort() + .map((fieldName) => ({ + id: fieldName, + displayAsText: fieldName.split('.')[1], + defaultSortDirection: Direction.asc, + })); + + if (!isEqual(columns, newColumns)) { + setColumns(newColumns); + setVisibleColumns(map('id', newColumns)); + } + }, [columns, results]); + + return ( + + + + ); +}; + +export const ActionResultsTable = React.memo(ActionResultsTableComponent); diff --git a/x-pack/plugins/osquery/public/action_results/helpers.ts b/x-pack/plugins/osquery/public/action_results/helpers.ts new file mode 100644 index 0000000000000..9f908e16c2eb2 --- /dev/null +++ b/x-pack/plugins/osquery/public/action_results/helpers.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 { + PaginationInputPaginated, + FactoryQueryTypes, + StrategyResponseType, + Inspect, +} from '../../common/search_strategy'; + +export type InspectResponse = Inspect & { response: string[] }; + +export const generateTablePaginationOptions = ( + activePage: number, + limit: number, + isBucketSort?: boolean +): PaginationInputPaginated => { + const cursorStart = activePage * limit; + return { + activePage, + cursorStart, + fakePossibleCount: 4 <= activePage && activePage > 0 ? limit * (activePage + 2) : limit * 5, + querySize: isBucketSort ? limit : limit + cursorStart, + }; +}; + +export const getInspectResponse = ( + response: StrategyResponseType, + prevResponse: InspectResponse +): InspectResponse => ({ + dsl: response?.inspect?.dsl ?? prevResponse?.dsl ?? [], + response: + response != null ? [JSON.stringify(response.rawResponse, null, 2)] : prevResponse?.response, +}); diff --git a/x-pack/plugins/osquery/public/action_results/translations.ts b/x-pack/plugins/osquery/public/action_results/translations.ts new file mode 100644 index 0000000000000..54c8ecebc60c0 --- /dev/null +++ b/x-pack/plugins/osquery/public/action_results/translations.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const ERROR_ALL_RESULTS = i18n.translate('xpack.osquery.results.errorSearchDescription', { + defaultMessage: `An error has occurred on all results search`, +}); + +export const FAIL_ALL_RESULTS = i18n.translate('xpack.osquery.results.failSearchDescription', { + defaultMessage: `Failed to fetch results`, +}); diff --git a/x-pack/plugins/osquery/public/action_results/use_action_results.ts b/x-pack/plugins/osquery/public/action_results/use_action_results.ts new file mode 100644 index 0000000000000..2c54606bf3fbb --- /dev/null +++ b/x-pack/plugins/osquery/public/action_results/use_action_results.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 deepEqual from 'fast-deep-equal'; +import { useCallback, useEffect, useRef, useState } from 'react'; + +import { createFilter } from '../common/helpers'; +import { useKibana } from '../common/lib/kibana'; +import { + ResultEdges, + PageInfoPaginated, + DocValueFields, + OsqueryQueries, + ResultsRequestOptions, + ResultsStrategyResponse, + Direction, +} from '../../common/search_strategy'; +import { ESTermQuery } from '../../common/typed_json'; + +import * as i18n from './translations'; +import { isCompleteResponse, isErrorResponse } from '../../../../../src/plugins/data/common'; +import { AbortError } from '../../../../../src/plugins/kibana_utils/common'; +import { generateTablePaginationOptions, getInspectResponse, InspectResponse } from './helpers'; + +const ID = 'resultsAllQuery'; + +export interface ResultsArgs { + results: ResultEdges; + id: string; + inspect: InspectResponse; + isInspected: boolean; + pageInfo: PageInfoPaginated; + totalCount: number; +} + +interface UseAllResults { + actionId: string; + activePage: number; + direction: Direction; + limit: number; + sortField: string; + docValueFields?: DocValueFields[]; + filterQuery?: ESTermQuery | string; + skip?: boolean; +} + +export const useAllResults = ({ + actionId, + activePage, + direction, + limit, + sortField, + docValueFields, + filterQuery, + skip = false, +}: UseAllResults): [boolean, ResultsArgs] => { + const { data, notifications } = useKibana().services; + + const abortCtrl = useRef(new AbortController()); + const [loading, setLoading] = useState(false); + const [resultsRequest, setHostRequest] = useState(null); + + const [resultsResponse, setResultsResponse] = useState({ + results: [], + id: ID, + inspect: { + dsl: [], + response: [], + }, + isInspected: false, + pageInfo: { + activePage: 0, + fakeTotalCount: 0, + showMorePagesIndicator: false, + }, + totalCount: -1, + }); + + const resultsSearch = useCallback( + (request: ResultsRequestOptions | null) => { + if (request == null || skip) { + return; + } + + let didCancel = false; + const asyncSearch = async () => { + abortCtrl.current = new AbortController(); + setLoading(true); + + const searchSubscription$ = data.search + .search(request, { + strategy: 'osquerySearchStrategy', + abortSignal: abortCtrl.current.signal, + }) + .subscribe({ + next: (response) => { + if (isCompleteResponse(response)) { + if (!didCancel) { + setLoading(false); + setResultsResponse((prevResponse) => ({ + ...prevResponse, + results: response.edges, + inspect: getInspectResponse(response, prevResponse.inspect), + pageInfo: response.pageInfo, + totalCount: response.totalCount, + })); + } + searchSubscription$.unsubscribe(); + } else if (isErrorResponse(response)) { + if (!didCancel) { + setLoading(false); + } + // TODO: Make response error status clearer + notifications.toasts.addWarning(i18n.ERROR_ALL_RESULTS); + searchSubscription$.unsubscribe(); + } + }, + error: (msg) => { + if (!(msg instanceof AbortError)) { + notifications.toasts.addDanger({ title: i18n.FAIL_ALL_RESULTS, text: msg.message }); + } + }, + }); + }; + abortCtrl.current.abort(); + asyncSearch(); + return () => { + didCancel = true; + abortCtrl.current.abort(); + }; + }, + [data.search, notifications.toasts, skip] + ); + + useEffect(() => { + setHostRequest((prevRequest) => { + const myRequest = { + ...(prevRequest ?? {}), + actionId, + docValueFields: docValueFields ?? [], + factoryQueryType: OsqueryQueries.actionResults, + filterQuery: createFilter(filterQuery), + pagination: generateTablePaginationOptions(activePage, limit), + sort: { + direction, + field: sortField, + }, + }; + if (!deepEqual(prevRequest, myRequest)) { + return myRequest; + } + return prevRequest; + }); + }, [actionId, activePage, direction, docValueFields, filterQuery, limit, sortField]); + + useEffect(() => { + resultsSearch(resultsRequest); + }, [resultsRequest, resultsSearch]); + + return [loading, resultsResponse]; +}; diff --git a/x-pack/plugins/osquery/public/actions/actions_table.tsx b/x-pack/plugins/osquery/public/actions/actions_table.tsx new file mode 100644 index 0000000000000..917e915d9d9dc --- /dev/null +++ b/x-pack/plugins/osquery/public/actions/actions_table.tsx @@ -0,0 +1,107 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { isEmpty, isEqual, keys, map } from 'lodash/fp'; +import { EuiDataGrid, EuiDataGridProps, EuiDataGridColumn, EuiDataGridSorting } from '@elastic/eui'; +import React, { createContext, useEffect, useState, useCallback, useContext, useMemo } from 'react'; + +import { useAllActions } from './use_all_actions'; +import { ActionEdges, Direction } from '../../common/search_strategy'; + +const DataContext = createContext([]); + +const ActionsTableComponent = () => { + const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 50 }); + const onChangeItemsPerPage = useCallback( + (pageSize) => + setPagination((currentPagination) => ({ + ...currentPagination, + pageSize, + pageIndex: 0, + })), + [setPagination] + ); + const onChangePage = useCallback( + (pageIndex) => setPagination((currentPagination) => ({ ...currentPagination, pageIndex })), + [setPagination] + ); + + const [columns, setColumns] = useState([]); + + // ** Sorting config + const [sortingColumns, setSortingColumns] = useState([]); + + const [, { actions, totalCount }] = useAllActions({ + activePage: pagination.pageIndex, + limit: pagination.pageSize, + direction: Direction.asc, + sortField: '@timestamp', + }); + + // Column visibility + const [visibleColumns, setVisibleColumns] = useState([]); // initialize to the full set of columns + + const columnVisibility = useMemo(() => ({ visibleColumns, setVisibleColumns }), [ + visibleColumns, + setVisibleColumns, + ]); + + const renderCellValue: EuiDataGridProps['renderCellValue'] = useMemo(() => { + return ({ rowIndex, columnId, setCellProps }) => { + // eslint-disable-next-line react-hooks/rules-of-hooks + const data = useContext(DataContext); + const value = data[rowIndex].fields[columnId]; + + return !isEmpty(value) ? value : '-'; + }; + }, []); + + const tableSorting: EuiDataGridSorting = useMemo( + () => ({ columns: sortingColumns, onSort: setSortingColumns }), + [setSortingColumns, sortingColumns] + ); + + const tablePagination = useMemo( + () => ({ + ...pagination, + pageSizeOptions: [10, 50, 100], + onChangeItemsPerPage, + onChangePage, + }), + [onChangeItemsPerPage, onChangePage, pagination] + ); + + useEffect(() => { + const newColumns = keys(actions[0]?.fields) + .sort() + .map((fieldName) => ({ + id: fieldName, + displayAsText: fieldName.split('.')[1], + defaultSortDirection: Direction.asc, + })); + + if (!isEqual(columns, newColumns)) { + setColumns(newColumns); + setVisibleColumns(map('id', newColumns)); + } + }, [columns, actions]); + + return ( + + + + ); +}; + +export const ActionsTable = React.memo(ActionsTableComponent); diff --git a/x-pack/plugins/osquery/public/actions/helpers.ts b/x-pack/plugins/osquery/public/actions/helpers.ts new file mode 100644 index 0000000000000..9f908e16c2eb2 --- /dev/null +++ b/x-pack/plugins/osquery/public/actions/helpers.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 { + PaginationInputPaginated, + FactoryQueryTypes, + StrategyResponseType, + Inspect, +} from '../../common/search_strategy'; + +export type InspectResponse = Inspect & { response: string[] }; + +export const generateTablePaginationOptions = ( + activePage: number, + limit: number, + isBucketSort?: boolean +): PaginationInputPaginated => { + const cursorStart = activePage * limit; + return { + activePage, + cursorStart, + fakePossibleCount: 4 <= activePage && activePage > 0 ? limit * (activePage + 2) : limit * 5, + querySize: isBucketSort ? limit : limit + cursorStart, + }; +}; + +export const getInspectResponse = ( + response: StrategyResponseType, + prevResponse: InspectResponse +): InspectResponse => ({ + dsl: response?.inspect?.dsl ?? prevResponse?.dsl ?? [], + response: + response != null ? [JSON.stringify(response.rawResponse, null, 2)] : prevResponse?.response, +}); diff --git a/x-pack/plugins/osquery/public/actions/translations.ts b/x-pack/plugins/osquery/public/actions/translations.ts new file mode 100644 index 0000000000000..3bf2d81e5e092 --- /dev/null +++ b/x-pack/plugins/osquery/public/actions/translations.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 { i18n } from '@kbn/i18n'; + +export const ERROR_ALL_ACTIONS = i18n.translate('xpack.osquery.actions.errorSearchDescription', { + defaultMessage: `An error has occurred on all actions search`, +}); + +export const FAIL_ALL_ACTIONS = i18n.translate('xpack.osquery.actions.failSearchDescription', { + defaultMessage: `Failed to fetch actions`, +}); + +export const ERROR_ACTION_DETAILS = i18n.translate( + 'xpack.osquery.actionDetails.errorSearchDescription', + { + defaultMessage: `An error has occurred on action details search`, + } +); + +export const FAIL_ACTION_DETAILS = i18n.translate( + 'xpack.osquery.actionDetails.failSearchDescription', + { + defaultMessage: `Failed to fetch action details`, + } +); + +export const ERROR_ACTION_RESULTS = i18n.translate( + 'xpack.osquery.actionResults.errorSearchDescription', + { + defaultMessage: `An error has occurred on action results search`, + } +); + +export const FAIL_ACTION_RESULTS = i18n.translate( + 'xpack.osquery.actionResults.failSearchDescription', + { + defaultMessage: `Failed to fetch action results`, + } +); diff --git a/x-pack/plugins/osquery/public/actions/use_action_details.ts b/x-pack/plugins/osquery/public/actions/use_action_details.ts new file mode 100644 index 0000000000000..3112d7cbf221e --- /dev/null +++ b/x-pack/plugins/osquery/public/actions/use_action_details.ts @@ -0,0 +1,141 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 deepEqual from 'fast-deep-equal'; +import { useCallback, useEffect, useRef, useState } from 'react'; + +import { createFilter } from '../common/helpers'; +import { useKibana } from '../common/lib/kibana'; +import { + DocValueFields, + OsqueryQueries, + ActionDetailsRequestOptions, + ActionDetailsStrategyResponse, +} from '../../common/search_strategy'; +import { ESTermQuery } from '../../common/typed_json'; + +import * as i18n from './translations'; +import { isCompleteResponse, isErrorResponse } from '../../../../../src/plugins/data/common'; +import { AbortError } from '../../../../../src/plugins/kibana_utils/common'; +import { getInspectResponse, InspectResponse } from './helpers'; + +const ID = 'actionDetailsQuery'; + +export interface ActionDetailsArgs { + actionDetails: Record; + id: string; + inspect: InspectResponse; + isInspected: boolean; +} + +interface UseActionDetails { + actionId: string; + docValueFields?: DocValueFields[]; + filterQuery?: ESTermQuery | string; + skip?: boolean; +} + +export const useActionDetails = ({ + actionId, + docValueFields, + filterQuery, + skip = false, +}: UseActionDetails): [boolean, ActionDetailsArgs] => { + const { data, notifications } = useKibana().services; + + const abortCtrl = useRef(new AbortController()); + const [loading, setLoading] = useState(false); + const [actionDetailsRequest, setHostRequest] = useState(null); + + const [actionDetailsResponse, setActionDetailsResponse] = useState({ + actionDetails: {}, + id: ID, + inspect: { + dsl: [], + response: [], + }, + isInspected: false, + }); + + const actionDetailsSearch = useCallback( + (request: ActionDetailsRequestOptions | null) => { + if (request == null || skip) { + return; + } + + let didCancel = false; + const asyncSearch = async () => { + abortCtrl.current = new AbortController(); + setLoading(true); + + const searchSubscription$ = data.search + .search(request, { + strategy: 'osquerySearchStrategy', + abortSignal: abortCtrl.current.signal, + }) + .subscribe({ + next: (response) => { + if (isCompleteResponse(response)) { + if (!didCancel) { + setLoading(false); + setActionDetailsResponse((prevResponse) => ({ + ...prevResponse, + actionDetails: response.actionDetails, + inspect: getInspectResponse(response, prevResponse.inspect), + })); + } + searchSubscription$.unsubscribe(); + } else if (isErrorResponse(response)) { + if (!didCancel) { + setLoading(false); + } + // TODO: Make response error status clearer + notifications.toasts.addWarning(i18n.ERROR_ACTION_DETAILS); + searchSubscription$.unsubscribe(); + } + }, + error: (msg) => { + if (!(msg instanceof AbortError)) { + notifications.toasts.addDanger({ + title: i18n.FAIL_ACTION_DETAILS, + text: msg.message, + }); + } + }, + }); + }; + abortCtrl.current.abort(); + asyncSearch(); + return () => { + didCancel = true; + abortCtrl.current.abort(); + }; + }, + [data.search, notifications.toasts, skip] + ); + + useEffect(() => { + setHostRequest((prevRequest) => { + const myRequest = { + ...(prevRequest ?? {}), + actionId, + docValueFields: docValueFields ?? [], + factoryQueryType: OsqueryQueries.actionDetails, + filterQuery: createFilter(filterQuery), + }; + if (!deepEqual(prevRequest, myRequest)) { + return myRequest; + } + return prevRequest; + }); + }, [actionId, docValueFields, filterQuery]); + + useEffect(() => { + actionDetailsSearch(actionDetailsRequest); + }, [actionDetailsRequest, actionDetailsSearch]); + + return [loading, actionDetailsResponse]; +}; diff --git a/x-pack/plugins/osquery/public/actions/use_all_actions.ts b/x-pack/plugins/osquery/public/actions/use_all_actions.ts new file mode 100644 index 0000000000000..192f5b1eb410c --- /dev/null +++ b/x-pack/plugins/osquery/public/actions/use_all_actions.ts @@ -0,0 +1,161 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 deepEqual from 'fast-deep-equal'; +import { useCallback, useEffect, useRef, useState } from 'react'; + +import { createFilter } from '../common/helpers'; +import { useKibana } from '../common/lib/kibana'; +import { + ActionEdges, + PageInfoPaginated, + DocValueFields, + OsqueryQueries, + ActionsRequestOptions, + ActionsStrategyResponse, + Direction, +} from '../../common/search_strategy'; +import { ESTermQuery } from '../../common/typed_json'; + +import * as i18n from './translations'; +import { isCompleteResponse, isErrorResponse } from '../../../../../src/plugins/data/common'; +import { AbortError } from '../../../../../src/plugins/kibana_utils/common'; +import { generateTablePaginationOptions, getInspectResponse, InspectResponse } from './helpers'; + +const ID = 'actionsAllQuery'; + +export interface ActionsArgs { + actions: ActionEdges; + id: string; + inspect: InspectResponse; + isInspected: boolean; + pageInfo: PageInfoPaginated; + totalCount: number; +} + +interface UseAllActions { + activePage: number; + direction: Direction; + limit: number; + sortField: string; + docValueFields?: DocValueFields[]; + filterQuery?: ESTermQuery | string; + skip?: boolean; +} + +export const useAllActions = ({ + activePage, + direction, + limit, + sortField, + docValueFields, + filterQuery, + skip = false, +}: UseAllActions): [boolean, ActionsArgs] => { + const { data, notifications } = useKibana().services; + + const abortCtrl = useRef(new AbortController()); + const [loading, setLoading] = useState(false); + const [actionsRequest, setHostRequest] = useState(null); + + const [actionsResponse, setActionsResponse] = useState({ + actions: [], + id: ID, + inspect: { + dsl: [], + response: [], + }, + isInspected: false, + pageInfo: { + activePage: 0, + fakeTotalCount: 0, + showMorePagesIndicator: false, + }, + totalCount: -1, + }); + + const actionsSearch = useCallback( + (request: ActionsRequestOptions | null) => { + if (request == null || skip) { + return; + } + + let didCancel = false; + const asyncSearch = async () => { + abortCtrl.current = new AbortController(); + setLoading(true); + + const searchSubscription$ = data.search + .search(request, { + strategy: 'osquerySearchStrategy', + abortSignal: abortCtrl.current.signal, + }) + .subscribe({ + next: (response) => { + if (isCompleteResponse(response)) { + if (!didCancel) { + setLoading(false); + setActionsResponse((prevResponse) => ({ + ...prevResponse, + actions: response.edges, + inspect: getInspectResponse(response, prevResponse.inspect), + pageInfo: response.pageInfo, + totalCount: response.totalCount, + })); + } + searchSubscription$.unsubscribe(); + } else if (isErrorResponse(response)) { + if (!didCancel) { + setLoading(false); + } + // TODO: Make response error status clearer + notifications.toasts.addWarning(i18n.ERROR_ALL_ACTIONS); + searchSubscription$.unsubscribe(); + } + }, + error: (msg) => { + if (!(msg instanceof AbortError)) { + notifications.toasts.addDanger({ title: i18n.FAIL_ALL_ACTIONS, text: msg.message }); + } + }, + }); + }; + abortCtrl.current.abort(); + asyncSearch(); + return () => { + didCancel = true; + abortCtrl.current.abort(); + }; + }, + [data.search, notifications.toasts, skip] + ); + + useEffect(() => { + setHostRequest((prevRequest) => { + const myRequest = { + ...(prevRequest ?? {}), + docValueFields: docValueFields ?? [], + factoryQueryType: OsqueryQueries.actions, + filterQuery: createFilter(filterQuery), + pagination: generateTablePaginationOptions(activePage, limit), + sort: { + direction, + field: sortField, + }, + }; + if (!deepEqual(prevRequest, myRequest)) { + return myRequest; + } + return prevRequest; + }); + }, [activePage, direction, docValueFields, filterQuery, limit, sortField]); + + useEffect(() => { + actionsSearch(actionsRequest); + }, [actionsRequest, actionsSearch]); + + return [loading, actionsResponse]; +}; diff --git a/x-pack/plugins/osquery/public/agents/agents_table.tsx b/x-pack/plugins/osquery/public/agents/agents_table.tsx new file mode 100644 index 0000000000000..1c0083b8252e8 --- /dev/null +++ b/x-pack/plugins/osquery/public/agents/agents_table.tsx @@ -0,0 +1,149 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { find } from 'lodash/fp'; +import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react'; +import { + EuiBasicTable, + EuiBasicTableColumn, + EuiBasicTableProps, + EuiTableSelectionType, + EuiHealth, +} from '@elastic/eui'; + +import { useAllAgents } from './use_all_agents'; +import { Direction } from '../../common/search_strategy'; +import { Agent } from '../../common/shared_imports'; + +interface AgentsTableProps { + selectedAgents: string[]; + onChange: (payload: string[]) => void; +} + +const AgentsTableComponent: React.FC = ({ selectedAgents, onChange }) => { + const [pageIndex, setPageIndex] = useState(0); + const [pageSize, setPageSize] = useState(5); + const [sortField, setSortField] = useState('id'); + const [sortDirection, setSortDirection] = useState(Direction.asc); + const [selectedItems, setSelectedItems] = useState([]); + const tableRef = useRef>(null); + + const onTableChange: EuiBasicTableProps['onChange'] = useCallback( + ({ page = {}, sort = {} }) => { + const { index: newPageIndex, size: newPageSize } = page; + + const { field: newSortField, direction: newSortDirection } = sort; + + setPageIndex(newPageIndex); + setPageSize(newPageSize); + setSortField(newSortField); + setSortDirection(newSortDirection); + }, + [] + ); + + const onSelectionChange: EuiTableSelectionType<{}>['onSelectionChange'] = useCallback( + (newSelectedItems) => { + setSelectedItems(newSelectedItems); + // @ts-expect-error + onChange(newSelectedItems.map((item) => item._id)); + }, + [onChange] + ); + + const renderStatus = (online: string) => { + const color = online ? 'success' : 'danger'; + const label = online ? 'Online' : 'Offline'; + return {label}; + }; + + const [, { agents, totalCount }] = useAllAgents({ + activePage: pageIndex, + limit: pageSize, + direction: sortDirection, + sortField, + }); + + const columns: Array> = useMemo( + () => [ + { + field: 'local_metadata.elastic.agent.id', + name: 'id', + sortable: true, + truncateText: true, + }, + { + field: 'local_metadata.host.name', + name: 'hostname', + truncateText: true, + }, + + { + field: 'active', + name: 'Online', + dataType: 'boolean', + render: (active: string) => renderStatus(active), + }, + ], + [] + ); + + const pagination = useMemo( + () => ({ + pageIndex, + pageSize, + totalItemCount: totalCount, + pageSizeOptions: [3, 5, 8], + }), + [pageIndex, pageSize, totalCount] + ); + + const sorting = useMemo( + () => ({ + sort: { + field: sortField, + direction: sortDirection, + }, + }), + [sortDirection, sortField] + ); + + const selection: EuiBasicTableProps['selection'] = useMemo( + () => ({ + selectable: (agent: Agent) => agent.active, + selectableMessage: (selectable: boolean) => (!selectable ? 'User is currently offline' : ''), + onSelectionChange, + initialSelected: selectedItems, + }), + [onSelectionChange, selectedItems] + ); + + useEffect(() => { + if (selectedAgents?.length && agents.length && selectedItems.length !== selectedAgents.length) { + tableRef?.current?.setSelection( + // @ts-expect-error + selectedAgents.map((agentId) => find({ _id: agentId }, agents)) + ); + } + }, [selectedAgents, agents, selectedItems.length]); + + return ( + + ref={tableRef} + items={agents} + itemId="_id" + columns={columns} + pagination={pagination} + sorting={sorting} + isSelectable={true} + selection={selection} + onChange={onTableChange} + rowHeader="firstName" + /> + ); +}; + +export const AgentsTable = React.memo(AgentsTableComponent); diff --git a/x-pack/plugins/osquery/public/agents/helpers.ts b/x-pack/plugins/osquery/public/agents/helpers.ts new file mode 100644 index 0000000000000..9f908e16c2eb2 --- /dev/null +++ b/x-pack/plugins/osquery/public/agents/helpers.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 { + PaginationInputPaginated, + FactoryQueryTypes, + StrategyResponseType, + Inspect, +} from '../../common/search_strategy'; + +export type InspectResponse = Inspect & { response: string[] }; + +export const generateTablePaginationOptions = ( + activePage: number, + limit: number, + isBucketSort?: boolean +): PaginationInputPaginated => { + const cursorStart = activePage * limit; + return { + activePage, + cursorStart, + fakePossibleCount: 4 <= activePage && activePage > 0 ? limit * (activePage + 2) : limit * 5, + querySize: isBucketSort ? limit : limit + cursorStart, + }; +}; + +export const getInspectResponse = ( + response: StrategyResponseType, + prevResponse: InspectResponse +): InspectResponse => ({ + dsl: response?.inspect?.dsl ?? prevResponse?.dsl ?? [], + response: + response != null ? [JSON.stringify(response.rawResponse, null, 2)] : prevResponse?.response, +}); diff --git a/x-pack/plugins/osquery/public/agents/translations.ts b/x-pack/plugins/osquery/public/agents/translations.ts new file mode 100644 index 0000000000000..a95ad5e4ce163 --- /dev/null +++ b/x-pack/plugins/osquery/public/agents/translations.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const ERROR_ALL_AGENTS = i18n.translate('xpack.osquery.agents.errorSearchDescription', { + defaultMessage: `An error has occurred on all agents search`, +}); + +export const FAIL_ALL_AGENTS = i18n.translate('xpack.osquery.agents.failSearchDescription', { + defaultMessage: `Failed to fetch agents`, +}); diff --git a/x-pack/plugins/osquery/public/agents/use_all_agents.ts b/x-pack/plugins/osquery/public/agents/use_all_agents.ts new file mode 100644 index 0000000000000..ad1a09486961a --- /dev/null +++ b/x-pack/plugins/osquery/public/agents/use_all_agents.ts @@ -0,0 +1,161 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 deepEqual from 'fast-deep-equal'; +import { useCallback, useEffect, useRef, useState } from 'react'; + +import { createFilter } from '../common/helpers'; +import { useKibana } from '../common/lib/kibana'; +import { + PageInfoPaginated, + DocValueFields, + OsqueryQueries, + AgentsRequestOptions, + AgentsStrategyResponse, + Direction, +} from '../../common/search_strategy'; +import { ESTermQuery } from '../../common/typed_json'; +import { Agent } from '../../common/shared_imports'; + +import * as i18n from './translations'; +import { isCompleteResponse, isErrorResponse } from '../../../../../src/plugins/data/common'; +import { AbortError } from '../../../../../src/plugins/kibana_utils/common'; +import { generateTablePaginationOptions, getInspectResponse, InspectResponse } from './helpers'; + +const ID = 'agentsAllQuery'; + +export interface AgentsArgs { + agents: Agent[]; + id: string; + inspect: InspectResponse; + isInspected: boolean; + pageInfo: PageInfoPaginated; + totalCount: number; +} + +interface UseAllAgents { + activePage: number; + direction: Direction; + limit: number; + sortField: string; + docValueFields?: DocValueFields[]; + filterQuery?: ESTermQuery | string; + skip?: boolean; +} + +export const useAllAgents = ({ + activePage, + direction, + limit, + sortField, + docValueFields, + filterQuery, + skip = false, +}: UseAllAgents): [boolean, AgentsArgs] => { + const { data, notifications } = useKibana().services; + + const abortCtrl = useRef(new AbortController()); + const [loading, setLoading] = useState(false); + const [agentsRequest, setHostRequest] = useState(null); + + const [agentsResponse, setAgentsResponse] = useState({ + agents: [], + id: ID, + inspect: { + dsl: [], + response: [], + }, + isInspected: false, + pageInfo: { + activePage: 0, + fakeTotalCount: 0, + showMorePagesIndicator: false, + }, + totalCount: -1, + }); + + const agentsSearch = useCallback( + (request: AgentsRequestOptions | null) => { + if (request == null || skip) { + return; + } + + let didCancel = false; + const asyncSearch = async () => { + abortCtrl.current = new AbortController(); + setLoading(true); + + const searchSubscription$ = data.search + .search(request, { + strategy: 'osquerySearchStrategy', + abortSignal: abortCtrl.current.signal, + }) + .subscribe({ + next: (response) => { + if (isCompleteResponse(response)) { + if (!didCancel) { + setLoading(false); + setAgentsResponse((prevResponse) => ({ + ...prevResponse, + agents: response.edges, + inspect: getInspectResponse(response, prevResponse.inspect), + pageInfo: response.pageInfo, + totalCount: response.totalCount, + })); + } + searchSubscription$.unsubscribe(); + } else if (isErrorResponse(response)) { + if (!didCancel) { + setLoading(false); + } + // TODO: Make response error status clearer + notifications.toasts.addWarning(i18n.ERROR_ALL_AGENTS); + searchSubscription$.unsubscribe(); + } + }, + error: (msg) => { + if (!(msg instanceof AbortError)) { + notifications.toasts.addDanger({ title: i18n.FAIL_ALL_AGENTS, text: msg.message }); + } + }, + }); + }; + abortCtrl.current.abort(); + asyncSearch(); + return () => { + didCancel = true; + abortCtrl.current.abort(); + }; + }, + [data.search, notifications.toasts, skip] + ); + + useEffect(() => { + setHostRequest((prevRequest) => { + const myRequest = { + ...(prevRequest ?? {}), + docValueFields: docValueFields ?? [], + factoryQueryType: OsqueryQueries.agents, + filterQuery: createFilter(filterQuery), + pagination: generateTablePaginationOptions(activePage, limit), + sort: { + direction, + field: sortField, + }, + }; + if (!deepEqual(prevRequest, myRequest)) { + return myRequest; + } + return prevRequest; + }); + }, [activePage, direction, docValueFields, filterQuery, limit, sortField]); + + useEffect(() => { + agentsSearch(agentsRequest); + }, [agentsRequest, agentsSearch]); + + return [loading, agentsResponse]; +}; diff --git a/x-pack/plugins/osquery/public/application.tsx b/x-pack/plugins/osquery/public/application.tsx new file mode 100644 index 0000000000000..1a5c826df3310 --- /dev/null +++ b/x-pack/plugins/osquery/public/application.tsx @@ -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 { EuiErrorBoundary } from '@elastic/eui'; +import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; +import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; +import React, { useMemo } from 'react'; +import ReactDOM from 'react-dom'; +import { Router } from 'react-router-dom'; +import { I18nProvider } from '@kbn/i18n/react'; +import { ThemeProvider } from 'styled-components'; + +import { useUiSetting$ } from '../../../../src/plugins/kibana_react/public'; +import { Storage } from '../../../../src/plugins/kibana_utils/public'; +import { AppMountParameters, CoreStart } from '../../../../src/core/public'; +import { AppPluginStartDependencies } from './types'; +import { OsqueryApp } from './components/app'; +import { DEFAULT_DARK_MODE, PLUGIN_NAME } from '../common'; +import { KibanaContextProvider } from './common/lib/kibana'; + +const OsqueryAppContext = () => { + const [darkMode] = useUiSetting$(DEFAULT_DARK_MODE); + const theme = useMemo( + () => ({ + eui: darkMode ? euiDarkVars : euiLightVars, + darkMode, + }), + [darkMode] + ); + + return ( + + + + ); +}; + +export const renderApp = ( + core: CoreStart, + services: AppPluginStartDependencies, + { element, history }: AppMountParameters, + storage: Storage, + kibanaVersion: string +) => { + ReactDOM.render( + + + + + + + + + , + element + ); + + return () => ReactDOM.unmountComponentAtNode(element); +}; diff --git a/x-pack/plugins/osquery/public/common/helpers.test.ts b/x-pack/plugins/osquery/public/common/helpers.test.ts new file mode 100644 index 0000000000000..5d378d79acc7a --- /dev/null +++ b/x-pack/plugins/osquery/public/common/helpers.test.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ESQuery } from '../../common/typed_json'; + +import { createFilter } from './helpers'; + +describe('Helpers', () => { + describe('#createFilter', () => { + test('if it is a string it returns untouched', () => { + const filter = createFilter('even invalid strings return the same'); + expect(filter).toBe('even invalid strings return the same'); + }); + + test('if it is an ESQuery object it will be returned as a string', () => { + const query: ESQuery = { term: { 'host.id': 'host-value' } }; + const filter = createFilter(query); + expect(filter).toBe(JSON.stringify(query)); + }); + + test('if it is undefined, then undefined is returned', () => { + const filter = createFilter(undefined); + expect(filter).toBe(undefined); + }); + }); +}); diff --git a/x-pack/plugins/osquery/public/common/helpers.ts b/x-pack/plugins/osquery/public/common/helpers.ts new file mode 100644 index 0000000000000..e922e030c9330 --- /dev/null +++ b/x-pack/plugins/osquery/public/common/helpers.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 { isString } from 'lodash/fp'; + +import { ESQuery } from '../../common/typed_json'; + +export const createFilter = (filterQuery: ESQuery | string | undefined) => + isString(filterQuery) ? filterQuery : JSON.stringify(filterQuery); diff --git a/x-pack/plugins/osquery/public/common/index.ts b/x-pack/plugins/osquery/public/common/index.ts new file mode 100644 index 0000000000000..d805555791e2a --- /dev/null +++ b/x-pack/plugins/osquery/public/common/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 './helpers'; diff --git a/x-pack/plugins/osquery/public/common/lib/kibana/index.ts b/x-pack/plugins/osquery/public/common/lib/kibana/index.ts new file mode 100644 index 0000000000000..b9cb71d4adb47 --- /dev/null +++ b/x-pack/plugins/osquery/public/common/lib/kibana/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 './kibana_react'; diff --git a/x-pack/plugins/osquery/public/common/lib/kibana/kibana_react.ts b/x-pack/plugins/osquery/public/common/lib/kibana/kibana_react.ts new file mode 100644 index 0000000000000..b4fb307a62b6c --- /dev/null +++ b/x-pack/plugins/osquery/public/common/lib/kibana/kibana_react.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { + KibanaContextProvider, + KibanaReactContextValue, + useKibana, + useUiSetting, + useUiSetting$, + withKibana, +} from '../../../../../../../src/plugins/kibana_react/public'; +import { StartServices } from '../../../types'; + +export type KibanaContext = KibanaReactContextValue; +export interface WithKibanaProps { + kibana: KibanaContext; +} + +const useTypedKibana = () => useKibana(); + +export { + KibanaContextProvider, + useTypedKibana as useKibana, + useUiSetting, + useUiSetting$, + withKibana, +}; diff --git a/x-pack/plugins/osquery/public/components/app.tsx b/x-pack/plugins/osquery/public/components/app.tsx new file mode 100644 index 0000000000000..49ff7e2bfb4da --- /dev/null +++ b/x-pack/plugins/osquery/public/components/app.tsx @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { FormattedMessage } from '@kbn/i18n/react'; +import { Switch, Route } from 'react-router-dom'; + +import { + EuiPage, + EuiPageBody, + EuiPageContent, + EuiPageContentBody, + EuiPageHeader, + EuiTitle, + EuiSpacer, +} from '@elastic/eui'; + +import { PLUGIN_NAME } from '../../common'; +import { LiveQuery } from '../live_query'; + +export const OsqueryAppComponent = () => { + return ( + + + + +

+ +

+
+
+ + + + + + + + + + + + + +
+
+ ); +}; + +export const OsqueryApp = React.memo(OsqueryAppComponent); diff --git a/x-pack/plugins/osquery/public/editor/index.tsx b/x-pack/plugins/osquery/public/editor/index.tsx new file mode 100644 index 0000000000000..a0e549e77467b --- /dev/null +++ b/x-pack/plugins/osquery/public/editor/index.tsx @@ -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 React, { useCallback } from 'react'; +import { EuiCodeEditor } from '@elastic/eui'; +import 'brace/mode/sql'; +import 'brace/theme/tomorrow'; +import 'brace/ext/language_tools'; + +const EDITOR_SET_OPTIONS = { + enableBasicAutocompletion: true, + enableLiveAutocompletion: true, +}; + +const EDITOR_PROPS = { + $blockScrolling: true, +}; + +interface OsqueryEditorProps { + defaultValue: string; + onChange: (newValue: string) => void; +} + +const OsqueryEditorComponent: React.FC = ({ defaultValue, onChange }) => { + const handleChange = useCallback( + (newValue) => { + onChange(newValue); + }, + [onChange] + ); + + return ( + + ); +}; + +export const OsqueryEditor = React.memo(OsqueryEditorComponent); diff --git a/x-pack/plugins/osquery/public/index.ts b/x-pack/plugins/osquery/public/index.ts new file mode 100644 index 0000000000000..32b0a30c24e7a --- /dev/null +++ b/x-pack/plugins/osquery/public/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 { PluginInitializerContext } from 'src/core/public'; +import { OsqueryPlugin } from './plugin'; + +// This exports static code and TypeScript types, +// as well as, Kibana Platform `plugin()` initializer. +export function plugin(initializerContext: PluginInitializerContext) { + return new OsqueryPlugin(initializerContext); +} +export { OsqueryPluginSetup, OsqueryPluginStart } from './types'; diff --git a/x-pack/plugins/osquery/public/live_query/edit/index.tsx b/x-pack/plugins/osquery/public/live_query/edit/index.tsx new file mode 100644 index 0000000000000..5626e78069d01 --- /dev/null +++ b/x-pack/plugins/osquery/public/live_query/edit/index.tsx @@ -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 { isEmpty } from 'lodash/fp'; +import { EuiSpacer } from '@elastic/eui'; +import React, { useCallback } from 'react'; +import { useParams } from 'react-router-dom'; + +import { useActionDetails } from '../../actions/use_action_details'; +import { ResultTabs } from './tabs'; +import { LiveQueryForm } from '../form'; + +const EditLiveQueryPageComponent = () => { + const { actionId } = useParams<{ actionId: string }>(); + const [loading, { actionDetails }] = useActionDetails({ actionId }); + + const handleSubmit = useCallback(() => Promise.resolve(), []); + + if (loading) { + return <>{'Loading...'}; + } + + return ( + <> + {!isEmpty(actionDetails) && ( + + )} + + + + ); +}; + +export const EditLiveQueryPage = React.memo(EditLiveQueryPageComponent); diff --git a/x-pack/plugins/osquery/public/live_query/edit/tabs.tsx b/x-pack/plugins/osquery/public/live_query/edit/tabs.tsx new file mode 100644 index 0000000000000..564b91873e11d --- /dev/null +++ b/x-pack/plugins/osquery/public/live_query/edit/tabs.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 { EuiTabbedContent, EuiSpacer } from '@elastic/eui'; +import React, { useCallback, useMemo } from 'react'; +import { useParams } from 'react-router-dom'; + +import { ResultsTable } from '../../results/results_table'; +import { ActionResultsTable } from '../../action_results/action_results_table'; + +const ResultTabsComponent = () => { + const { actionId } = useParams<{ actionId: string }>(); + const tabs = useMemo( + () => [ + { + id: 'status', + name: 'Status', + content: ( + <> + + + + ), + }, + { + id: 'results', + name: 'Results', + content: ( + <> + + + + ), + }, + ], + [actionId] + ); + + const handleTabClick = useCallback((tab) => { + // eslint-disable-next-line no-console + console.log('clicked tab', tab); + }, []); + + return ( + + ); +}; + +export const ResultTabs = React.memo(ResultTabsComponent); diff --git a/x-pack/plugins/osquery/public/live_query/form/agents_table_field.tsx b/x-pack/plugins/osquery/public/live_query/form/agents_table_field.tsx new file mode 100644 index 0000000000000..a6d7fbd404321 --- /dev/null +++ b/x-pack/plugins/osquery/public/live_query/form/agents_table_field.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useCallback } from 'react'; +import { FieldHook } from '../../shared_imports'; +import { AgentsTable } from '../../agents/agents_table'; + +interface AgentsTableFieldProps { + field: FieldHook; +} + +const AgentsTableFieldComponent: React.FC = ({ field }) => { + const { value, setValue } = field; + const handleChange = useCallback( + (props) => { + if (props !== value) { + return setValue(props); + } + }, + [value, setValue] + ); + + return ; +}; + +export const AgentsTableField = React.memo(AgentsTableFieldComponent); diff --git a/x-pack/plugins/osquery/public/live_query/form/code_editor_field.tsx b/x-pack/plugins/osquery/public/live_query/form/code_editor_field.tsx new file mode 100644 index 0000000000000..458d2263fe9c2 --- /dev/null +++ b/x-pack/plugins/osquery/public/live_query/form/code_editor_field.tsx @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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, { useCallback } from 'react'; + +import { OsqueryEditor } from '../../editor'; +import { FieldHook } from '../../shared_imports'; + +interface CodeEditorFieldProps { + field: FieldHook<{ query: string }>; +} + +const CodeEditorFieldComponent: React.FC = ({ field }) => { + const { value, setValue } = field; + const handleChange = useCallback( + (newQuery) => { + setValue({ + ...value, + query: newQuery, + }); + }, + [value, setValue] + ); + + return ; +}; + +export const CodeEditorField = React.memo(CodeEditorFieldComponent); diff --git a/x-pack/plugins/osquery/public/live_query/form/index.tsx b/x-pack/plugins/osquery/public/live_query/form/index.tsx new file mode 100644 index 0000000000000..23aa94b46a569 --- /dev/null +++ b/x-pack/plugins/osquery/public/live_query/form/index.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. + */ + +import { EuiButton, EuiSpacer } from '@elastic/eui'; +import React, { useCallback } from 'react'; + +import { UseField, Form, useForm } from '../../shared_imports'; +import { AgentsTableField } from './agents_table_field'; +import { CodeEditorField } from './code_editor_field'; + +const FORM_ID = 'liveQueryForm'; + +interface LiveQueryFormProps { + actionDetails?: Record; + onSubmit: (payload: Record) => Promise; +} + +const LiveQueryFormComponent: React.FC = ({ actionDetails, onSubmit }) => { + const handleSubmit = useCallback( + (payload) => { + onSubmit(payload); + return Promise.resolve(); + }, + [onSubmit] + ); + + const { form } = useForm({ + id: FORM_ID, + // schema: formSchema, + onSubmit: handleSubmit, + options: { + stripEmptyFields: false, + }, + defaultValue: actionDetails, + deserializer: ({ fields, _source }) => ({ + agents: fields?.agents, + command: _source?.data?.commands[0], + }), + }); + + const { submit } = form; + + return ( +
+ + + + Send query + + ); +}; + +export const LiveQueryForm = React.memo(LiveQueryFormComponent); diff --git a/x-pack/plugins/osquery/public/live_query/form/schema.ts b/x-pack/plugins/osquery/public/live_query/form/schema.ts new file mode 100644 index 0000000000000..e55b3d6cd6a5b --- /dev/null +++ b/x-pack/plugins/osquery/public/live_query/form/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 { FIELD_TYPES, FormSchema } from '../../shared_imports'; + +export const formSchema: FormSchema = { + agents: { + type: FIELD_TYPES.MULTI_SELECT, + }, + command: { + type: FIELD_TYPES.TEXTAREA, + validations: [], + }, +}; diff --git a/x-pack/plugins/osquery/public/live_query/index.tsx b/x-pack/plugins/osquery/public/live_query/index.tsx new file mode 100644 index 0000000000000..646d2637a4c40 --- /dev/null +++ b/x-pack/plugins/osquery/public/live_query/index.tsx @@ -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 React from 'react'; +import { Switch, Route, useRouteMatch } from 'react-router-dom'; + +import { QueriesPage } from './queries'; +import { NewLiveQueryPage } from './new'; +import { EditLiveQueryPage } from './edit'; + +const LiveQueryComponent = () => { + const match = useRouteMatch(); + + return ( + + + + + + + + + + + + ); +}; + +export const LiveQuery = React.memo(LiveQueryComponent); diff --git a/x-pack/plugins/osquery/public/live_query/new/index.tsx b/x-pack/plugins/osquery/public/live_query/new/index.tsx new file mode 100644 index 0000000000000..40f934b4690f9 --- /dev/null +++ b/x-pack/plugins/osquery/public/live_query/new/index.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useCallback } from 'react'; +import { useHistory } from 'react-router-dom'; + +import { useKibana } from '../../common/lib/kibana'; +import { LiveQueryForm } from '../form'; + +const NewLiveQueryPageComponent = () => { + const { http } = useKibana().services; + const history = useHistory(); + + const handleSubmit = useCallback( + async (props) => { + const response = await http.post('/api/osquery/queries', { body: JSON.stringify(props) }); + const requestParamsActionId = JSON.parse(response.meta.request.params.body).action_id; + history.push(`/live_query/queries/${requestParamsActionId}`); + }, + [history, http] + ); + + return ; +}; + +export const NewLiveQueryPage = React.memo(NewLiveQueryPageComponent); diff --git a/x-pack/plugins/osquery/public/live_query/queries/index.tsx b/x-pack/plugins/osquery/public/live_query/queries/index.tsx new file mode 100644 index 0000000000000..5600284b8c147 --- /dev/null +++ b/x-pack/plugins/osquery/public/live_query/queries/index.tsx @@ -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 { EuiSpacer, EuiTitle } from '@elastic/eui'; +import React from 'react'; + +import { ActionsTable } from '../../actions/actions_table'; + +const QueriesPageComponent = () => { + return ( + <> + +

{'Queries'}

+
+ + + + ); +}; + +export const QueriesPage = React.memo(QueriesPageComponent); diff --git a/x-pack/plugins/osquery/public/plugin.ts b/x-pack/plugins/osquery/public/plugin.ts new file mode 100644 index 0000000000000..41698d3a1740d --- /dev/null +++ b/x-pack/plugins/osquery/public/plugin.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { + AppMountParameters, + CoreSetup, + Plugin, + PluginInitializerContext, + CoreStart, +} from 'src/core/public'; +import { Storage } from '../../../../src/plugins/kibana_utils/public'; +import { OsqueryPluginSetup, OsqueryPluginStart, AppPluginStartDependencies } from './types'; +import { PLUGIN_NAME } from '../common'; + +export class OsqueryPlugin implements Plugin { + private kibanaVersion: string; + private storage = new Storage(localStorage); + + constructor(private readonly initializerContext: PluginInitializerContext) { + this.kibanaVersion = this.initializerContext.env.packageInfo.version; + } + + public setup(core: CoreSetup): OsqueryPluginSetup { + const config = this.initializerContext.config.get<{ enabled: boolean }>(); + + if (!config.enabled) { + return {}; + } + + const storage = this.storage; + const kibanaVersion = this.kibanaVersion; + // Register an application into the side navigation menu + core.application.register({ + id: 'osquery', + title: PLUGIN_NAME, + async mount(params: AppMountParameters) { + // Get start services as specified in kibana.json + const [coreStart, depsStart] = await core.getStartServices(); + // Load application bundle + const { renderApp } = await import('./application'); + // Render the application + return renderApp( + coreStart, + depsStart as AppPluginStartDependencies, + params, + storage, + kibanaVersion + ); + }, + }); + + // Return methods that should be available to other plugins + return {}; + } + + public start(core: CoreStart): OsqueryPluginStart { + return {}; + } + + public stop() {} +} diff --git a/x-pack/plugins/osquery/public/results/helpers.ts b/x-pack/plugins/osquery/public/results/helpers.ts new file mode 100644 index 0000000000000..9f908e16c2eb2 --- /dev/null +++ b/x-pack/plugins/osquery/public/results/helpers.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 { + PaginationInputPaginated, + FactoryQueryTypes, + StrategyResponseType, + Inspect, +} from '../../common/search_strategy'; + +export type InspectResponse = Inspect & { response: string[] }; + +export const generateTablePaginationOptions = ( + activePage: number, + limit: number, + isBucketSort?: boolean +): PaginationInputPaginated => { + const cursorStart = activePage * limit; + return { + activePage, + cursorStart, + fakePossibleCount: 4 <= activePage && activePage > 0 ? limit * (activePage + 2) : limit * 5, + querySize: isBucketSort ? limit : limit + cursorStart, + }; +}; + +export const getInspectResponse = ( + response: StrategyResponseType, + prevResponse: InspectResponse +): InspectResponse => ({ + dsl: response?.inspect?.dsl ?? prevResponse?.dsl ?? [], + response: + response != null ? [JSON.stringify(response.rawResponse, null, 2)] : prevResponse?.response, +}); diff --git a/x-pack/plugins/osquery/public/results/results_table.tsx b/x-pack/plugins/osquery/public/results/results_table.tsx new file mode 100644 index 0000000000000..69b350e461a5c --- /dev/null +++ b/x-pack/plugins/osquery/public/results/results_table.tsx @@ -0,0 +1,119 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { isEmpty, isEqual, keys, map } from 'lodash/fp'; +import { EuiDataGrid, EuiDataGridProps, EuiDataGridColumn } from '@elastic/eui'; +import React, { createContext, useEffect, useState, useCallback, useContext, useMemo } from 'react'; + +import { EuiDataGridSorting } from '@elastic/eui'; +import { useAllResults } from './use_all_results'; +import { Direction, ResultEdges } from '../../common/search_strategy'; + +const DataContext = createContext([]); + +interface ResultsTableComponentProps { + actionId: string; +} + +const ResultsTableComponent: React.FC = ({ actionId }) => { + const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 50 }); + const onChangeItemsPerPage = useCallback( + (pageSize) => + setPagination((currentPagination) => ({ + ...currentPagination, + pageSize, + pageIndex: 0, + })), + [setPagination] + ); + const onChangePage = useCallback( + (pageIndex) => setPagination((currentPagination) => ({ ...currentPagination, pageIndex })), + [setPagination] + ); + + const [columns, setColumns] = useState([]); + + // ** Sorting config + const [sortingColumns, setSortingColumns] = useState([]); + const onSort = useCallback( + (newSortingColumns) => { + setSortingColumns(newSortingColumns); + }, + [setSortingColumns] + ); + + const [, { results, totalCount }] = useAllResults({ + actionId, + activePage: pagination.pageIndex, + limit: pagination.pageSize, + direction: Direction.asc, + sortField: '@timestamp', + }); + + const [visibleColumns, setVisibleColumns] = useState([]); + const columnVisibility = useMemo(() => ({ visibleColumns, setVisibleColumns }), [ + visibleColumns, + setVisibleColumns, + ]); + + const renderCellValue: EuiDataGridProps['renderCellValue'] = useMemo( + () => ({ rowIndex, columnId, setCellProps }) => { + // eslint-disable-next-line react-hooks/rules-of-hooks + const data = useContext(DataContext); + + const value = data[rowIndex].fields[columnId]; + + return !isEmpty(value) ? value : '-'; + }, + [] + ); + + const tableSorting = useMemo(() => ({ columns: sortingColumns, onSort }), [ + onSort, + sortingColumns, + ]); + + const tablePagination = useMemo( + () => ({ + ...pagination, + pageSizeOptions: [10, 50, 100], + onChangeItemsPerPage, + onChangePage, + }), + [onChangeItemsPerPage, onChangePage, pagination] + ); + + useEffect(() => { + const newColumns: EuiDataGridColumn[] = keys(results[0]?.fields) + .sort() + .map((fieldName) => ({ + id: fieldName, + displayAsText: fieldName.split('.')[1], + defaultSortDirection: 'asc', + })); + + if (!isEqual(columns, newColumns)) { + setColumns(newColumns); + setVisibleColumns(map('id', newColumns)); + } + }, [columns, results]); + + return ( + + + + ); +}; + +export const ResultsTable = React.memo(ResultsTableComponent); diff --git a/x-pack/plugins/osquery/public/results/translations.ts b/x-pack/plugins/osquery/public/results/translations.ts new file mode 100644 index 0000000000000..54c8ecebc60c0 --- /dev/null +++ b/x-pack/plugins/osquery/public/results/translations.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const ERROR_ALL_RESULTS = i18n.translate('xpack.osquery.results.errorSearchDescription', { + defaultMessage: `An error has occurred on all results search`, +}); + +export const FAIL_ALL_RESULTS = i18n.translate('xpack.osquery.results.failSearchDescription', { + defaultMessage: `Failed to fetch results`, +}); diff --git a/x-pack/plugins/osquery/public/results/use_all_results.ts b/x-pack/plugins/osquery/public/results/use_all_results.ts new file mode 100644 index 0000000000000..2fc5f9ae869a7 --- /dev/null +++ b/x-pack/plugins/osquery/public/results/use_all_results.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 deepEqual from 'fast-deep-equal'; +import { useCallback, useEffect, useRef, useState } from 'react'; + +import { createFilter } from '../common/helpers'; +import { useKibana } from '../common/lib/kibana'; +import { + ResultEdges, + PageInfoPaginated, + DocValueFields, + OsqueryQueries, + ResultsRequestOptions, + ResultsStrategyResponse, + Direction, +} from '../../common/search_strategy'; +import { ESTermQuery } from '../../common/typed_json'; + +import * as i18n from './translations'; +import { isCompleteResponse, isErrorResponse } from '../../../../../src/plugins/data/common'; +import { AbortError } from '../../../../../src/plugins/kibana_utils/common'; +import { generateTablePaginationOptions, getInspectResponse, InspectResponse } from './helpers'; + +const ID = 'resultsAllQuery'; + +export interface ResultsArgs { + results: ResultEdges; + id: string; + inspect: InspectResponse; + isInspected: boolean; + pageInfo: PageInfoPaginated; + totalCount: number; +} + +interface UseAllResults { + actionId: string; + activePage: number; + direction: Direction; + limit: number; + sortField: string; + docValueFields?: DocValueFields[]; + filterQuery?: ESTermQuery | string; + skip?: boolean; +} + +export const useAllResults = ({ + actionId, + activePage, + direction, + limit, + sortField, + docValueFields, + filterQuery, + skip = false, +}: UseAllResults): [boolean, ResultsArgs] => { + const { data, notifications } = useKibana().services; + + const abortCtrl = useRef(new AbortController()); + const [loading, setLoading] = useState(false); + const [resultsRequest, setHostRequest] = useState(null); + + const [resultsResponse, setResultsResponse] = useState({ + results: [], + id: ID, + inspect: { + dsl: [], + response: [], + }, + isInspected: false, + pageInfo: { + activePage: 0, + fakeTotalCount: 0, + showMorePagesIndicator: false, + }, + totalCount: -1, + }); + + const resultsSearch = useCallback( + (request: ResultsRequestOptions | null) => { + if (request == null || skip) { + return; + } + + let didCancel = false; + const asyncSearch = async () => { + abortCtrl.current = new AbortController(); + setLoading(true); + + const searchSubscription$ = data.search + .search(request, { + strategy: 'osquerySearchStrategy', + abortSignal: abortCtrl.current.signal, + }) + .subscribe({ + next: (response) => { + if (isCompleteResponse(response)) { + if (!didCancel) { + setLoading(false); + setResultsResponse((prevResponse) => ({ + ...prevResponse, + results: response.edges, + inspect: getInspectResponse(response, prevResponse.inspect), + pageInfo: response.pageInfo, + totalCount: response.totalCount, + })); + } + searchSubscription$.unsubscribe(); + } else if (isErrorResponse(response)) { + if (!didCancel) { + setLoading(false); + } + // TODO: Make response error status clearer + notifications.toasts.addWarning(i18n.ERROR_ALL_RESULTS); + searchSubscription$.unsubscribe(); + } + }, + error: (msg) => { + if (!(msg instanceof AbortError)) { + notifications.toasts.addDanger({ title: i18n.FAIL_ALL_RESULTS, text: msg.message }); + } + }, + }); + }; + abortCtrl.current.abort(); + asyncSearch(); + return () => { + didCancel = true; + abortCtrl.current.abort(); + }; + }, + [data.search, notifications.toasts, skip] + ); + + useEffect(() => { + setHostRequest((prevRequest) => { + const myRequest = { + ...(prevRequest ?? {}), + actionId, + docValueFields: docValueFields ?? [], + factoryQueryType: OsqueryQueries.results, + filterQuery: createFilter(filterQuery), + pagination: generateTablePaginationOptions(activePage, limit), + sort: { + direction, + field: sortField, + }, + }; + if (!deepEqual(prevRequest, myRequest)) { + return myRequest; + } + return prevRequest; + }); + }, [actionId, activePage, direction, docValueFields, filterQuery, limit, sortField]); + + useEffect(() => { + resultsSearch(resultsRequest); + }, [resultsRequest, resultsSearch]); + + return [loading, resultsResponse]; +}; diff --git a/x-pack/plugins/osquery/public/shared_imports.ts b/x-pack/plugins/osquery/public/shared_imports.ts new file mode 100644 index 0000000000000..5f7503a00702c --- /dev/null +++ b/x-pack/plugins/osquery/public/shared_imports.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { + getUseField, + getFieldValidityAndErrorMessage, + FieldHook, + FieldValidateResponse, + FIELD_TYPES, + Form, + FormData, + FormDataProvider, + FormHook, + FormSchema, + UseField, + UseMultiFields, + useForm, + useFormContext, + useFormData, + ValidationError, + ValidationFunc, + VALIDATION_TYPES, +} from '../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; +export { Field, SelectField } from '../../../../src/plugins/es_ui_shared/static/forms/components'; +export { fieldValidators } from '../../../../src/plugins/es_ui_shared/static/forms/helpers'; +export { ERROR_CODE } from '../../../../src/plugins/es_ui_shared/static/forms/helpers/field_validators/types'; diff --git a/x-pack/plugins/osquery/public/types.ts b/x-pack/plugins/osquery/public/types.ts new file mode 100644 index 0000000000000..faaccfc29d5f1 --- /dev/null +++ b/x-pack/plugins/osquery/public/types.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 { DataPublicPluginStart } from '../../../../src/plugins/data/public'; +import { FleetStart } from '../../fleet/public'; +import { CoreStart } from '../../../../src/core/public'; +import { NavigationPublicPluginStart } from '../../../../src/plugins/navigation/public'; + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface OsqueryPluginSetup {} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface OsqueryPluginStart {} + +export interface AppPluginStartDependencies { + navigation: NavigationPublicPluginStart; +} + +export interface StartPlugins { + data: DataPublicPluginStart; + fleet?: FleetStart; +} + +export type StartServices = CoreStart & StartPlugins; diff --git a/x-pack/plugins/osquery/server/config.ts b/x-pack/plugins/osquery/server/config.ts new file mode 100644 index 0000000000000..633a95b8f91a7 --- /dev/null +++ b/x-pack/plugins/osquery/server/config.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { TypeOf, schema } from '@kbn/config-schema'; + +export const ConfigSchema = schema.object({ + enabled: schema.boolean({ defaultValue: false }), +}); + +export type ConfigType = TypeOf; diff --git a/x-pack/plugins/osquery/server/create_config.ts b/x-pack/plugins/osquery/server/create_config.ts new file mode 100644 index 0000000000000..e46c71798eb9f --- /dev/null +++ b/x-pack/plugins/osquery/server/create_config.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 { map } from 'rxjs/operators'; +import { PluginInitializerContext } from 'kibana/server'; +import { Observable } from 'rxjs'; + +import { ConfigType } from './config'; + +export const createConfig$ = ( + context: PluginInitializerContext +): Observable> => { + return context.config.create().pipe(map((config) => config)); +}; diff --git a/x-pack/plugins/osquery/server/index.ts b/x-pack/plugins/osquery/server/index.ts new file mode 100644 index 0000000000000..c74ef6c95a2e7 --- /dev/null +++ b/x-pack/plugins/osquery/server/index.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 { PluginInitializerContext } from '../../../../src/core/server'; +import { OsqueryPlugin } from './plugin'; +import { ConfigSchema } from './config'; + +export const config = { + schema: ConfigSchema, + exposeToBrowser: { + enabled: true, + }, +}; +export function plugin(initializerContext: PluginInitializerContext) { + return new OsqueryPlugin(initializerContext); +} + +export { OsqueryPluginSetup, OsqueryPluginStart } from './types'; diff --git a/x-pack/plugins/osquery/server/plugin.ts b/x-pack/plugins/osquery/server/plugin.ts new file mode 100644 index 0000000000000..3e59faa55d057 --- /dev/null +++ b/x-pack/plugins/osquery/server/plugin.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 { first } from 'rxjs/operators'; +import { + PluginInitializerContext, + CoreSetup, + CoreStart, + Plugin, + Logger, +} from '../../../../src/core/server'; + +import { createConfig$ } from './create_config'; +import { OsqueryPluginSetup, OsqueryPluginStart, SetupPlugins, StartPlugins } from './types'; +import { defineRoutes } from './routes'; +import { osquerySearchStrategyProvider } from './search_strategy/osquery'; + +export class OsqueryPlugin implements Plugin { + private readonly logger: Logger; + + constructor(private readonly initializerContext: PluginInitializerContext) { + this.logger = this.initializerContext.logger.get(); + } + + public async setup(core: CoreSetup, plugins: SetupPlugins) { + this.logger.debug('osquery: Setup'); + const config = await createConfig$(this.initializerContext).pipe(first()).toPromise(); + + if (!config.enabled) { + return {}; + } + + const router = core.http.createRouter(); + + // Register server side APIs + defineRoutes(router); + + core.getStartServices().then(([_, depsStart]) => { + const osquerySearchStrategy = osquerySearchStrategyProvider(depsStart.data); + + plugins.data.search.registerSearchStrategy('osquerySearchStrategy', osquerySearchStrategy); + }); + + return {}; + } + + public start(core: CoreStart) { + this.logger.debug('osquery: Started'); + return {}; + } + + public stop() {} +} diff --git a/x-pack/plugins/osquery/server/routes/index.ts b/x-pack/plugins/osquery/server/routes/index.ts new file mode 100644 index 0000000000000..f975865950a4d --- /dev/null +++ b/x-pack/plugins/osquery/server/routes/index.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 uuid from 'uuid'; +import { schema } from '@kbn/config-schema'; +import moment from 'moment'; + +import { IRouter } from '../../../../../src/core/server'; + +export function defineRoutes(router: IRouter) { + router.post( + { + path: '/api/osquery/queries', + validate: { + params: schema.object({}, { unknowns: 'allow' }), + body: schema.object({}, { unknowns: 'allow' }), + }, + }, + async (context, request, response) => { + const esClient = context.core.elasticsearch.client.asInternalUser; + const query = await esClient.index<{}, {}>({ + index: '.fleet-actions-new', + body: { + action_id: uuid.v4(), + '@timestamp': moment().toISOString(), + expiration: moment().add(2, 'days').toISOString(), + type: 'APP_ACTION', + input_id: 'osquery', + // @ts-expect-error + agents: request.body.agents, + data: { + commands: [ + { + id: uuid.v4(), + // @ts-expect-error + query: request.body.command.query, + }, + ], + }, + }, + }); + return response.ok({ + body: query, + }); + } + ); +} diff --git a/x-pack/plugins/osquery/server/search_strategy/osquery/factory/actions/all/index.ts b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/actions/all/index.ts new file mode 100644 index 0000000000000..75cdb67deed4d --- /dev/null +++ b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/actions/all/index.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 { IEsSearchResponse } from '../../../../../../../../../src/plugins/data/common'; +import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../../../../common/constants'; +import { + ActionsStrategyResponse, + ActionsRequestOptions, + OsqueryQueries, +} from '../../../../../../common/search_strategy/osquery'; +import { inspectStringifyObject } from '../../../../../../common/utils/build_query'; +import { OsqueryFactory } from '../../types'; +import { buildActionsQuery } from './query.all_actions.dsl'; + +export const allActions: OsqueryFactory = { + buildDsl: (options: ActionsRequestOptions) => { + if (options.pagination && options.pagination.querySize >= DEFAULT_MAX_TABLE_QUERY_SIZE) { + throw new Error(`No query size above ${DEFAULT_MAX_TABLE_QUERY_SIZE}`); + } + return buildActionsQuery(options); + }, + parse: async ( + options: ActionsRequestOptions, + response: IEsSearchResponse + ): Promise => { + const { activePage } = options.pagination; + const inspect = { + dsl: [inspectStringifyObject(buildActionsQuery(options))], + }; + + return { + ...response, + inspect, + edges: response.rawResponse.hits.hits, + totalCount: response.rawResponse.hits.total, + pageInfo: { + activePage: activePage ?? 0, + fakeTotalCount: 0, + showMorePagesIndicator: false, + }, + }; + }, +}; diff --git a/x-pack/plugins/osquery/server/search_strategy/osquery/factory/actions/all/query.all_actions.dsl.ts b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/actions/all/query.all_actions.dsl.ts new file mode 100644 index 0000000000000..29af1df3a9e0c --- /dev/null +++ b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/actions/all/query.all_actions.dsl.ts @@ -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 { ISearchRequestParams } from '../../../../../../../../../src/plugins/data/common'; +import { AgentsRequestOptions } from '../../../../../../common/search_strategy'; +import { createQueryFilterClauses } from '../../../../../../common/utils/build_query'; + +export const buildActionsQuery = ({ + docValueFields, + filterQuery, + pagination: { activePage, querySize }, + sort, +}: AgentsRequestOptions): ISearchRequestParams => { + const filter = [...createQueryFilterClauses(filterQuery)]; + + const dslQuery = { + allowNoIndices: true, + index: '.fleet-actions', + ignoreUnavailable: true, + body: { + query: { bool: { filter } }, + from: activePage * querySize, + size: querySize, + track_total_hits: true, + fields: ['*'], + }, + }; + + return dslQuery; +}; diff --git a/x-pack/plugins/osquery/server/search_strategy/osquery/factory/actions/details/index.ts b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/actions/details/index.ts new file mode 100644 index 0000000000000..09e317786e20f --- /dev/null +++ b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/actions/details/index.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 { IEsSearchResponse } from '../../../../../../../../../src/plugins/data/common'; +import { + ActionDetailsStrategyResponse, + ActionDetailsRequestOptions, + OsqueryQueries, +} from '../../../../../../common/search_strategy/osquery'; + +import { inspectStringifyObject } from '../../../../../../common/utils/build_query'; +import { OsqueryFactory } from '../../types'; +import { buildActionDetailsQuery } from './query.action_details.dsl'; + +export const actionDetails: OsqueryFactory = { + buildDsl: (options: ActionDetailsRequestOptions) => { + return buildActionDetailsQuery(options); + }, + parse: async ( + options: ActionDetailsRequestOptions, + response: IEsSearchResponse + ): Promise => { + const inspect = { + dsl: [inspectStringifyObject(buildActionDetailsQuery(options))], + }; + + return { + ...response, + inspect, + actionDetails: response.rawResponse.hits.hits[0], + }; + }, +}; diff --git a/x-pack/plugins/osquery/server/search_strategy/osquery/factory/actions/details/query.action_details.dsl.ts b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/actions/details/query.action_details.dsl.ts new file mode 100644 index 0000000000000..f22066134cbca --- /dev/null +++ b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/actions/details/query.action_details.dsl.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 { ISearchRequestParams } from '../../../../../../../../../src/plugins/data/common'; +import { ActionDetailsRequestOptions } from '../../../../../../common/search_strategy'; +import { createQueryFilterClauses } from '../../../../../../common/utils/build_query'; + +export const buildActionDetailsQuery = ({ + actionId, + docValueFields, + filterQuery, +}: ActionDetailsRequestOptions): ISearchRequestParams => { + const filter = [ + ...createQueryFilterClauses(filterQuery), + { + match_phrase: { + action_id: actionId, + }, + }, + ]; + + const dslQuery = { + allowNoIndices: true, + index: '.fleet-actions', + ignoreUnavailable: true, + body: { + query: { bool: { filter } }, + size: 1, + fields: ['*'], + }, + }; + + return dslQuery; +}; diff --git a/x-pack/plugins/osquery/server/search_strategy/osquery/factory/actions/index.ts b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/actions/index.ts new file mode 100644 index 0000000000000..c5a6342c180ed --- /dev/null +++ b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/actions/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 './all'; +export * from './details'; +export * from './results'; diff --git a/x-pack/plugins/osquery/server/search_strategy/osquery/factory/actions/results/index.ts b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/actions/results/index.ts new file mode 100644 index 0000000000000..4a049ca670cc6 --- /dev/null +++ b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/actions/results/index.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 { IEsSearchResponse } from '../../../../../../../../../src/plugins/data/common'; +import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../../../../common/constants'; +import { + ActionResultsStrategyResponse, + ActionResultsRequestOptions, + OsqueryQueries, +} from '../../../../../../common/search_strategy/osquery'; + +import { inspectStringifyObject } from '../../../../../../common/utils/build_query'; +import { OsqueryFactory } from '../../types'; +import { buildActionResultsQuery } from './query.action_results.dsl'; + +export const actionResults: OsqueryFactory = { + buildDsl: (options: ActionResultsRequestOptions) => { + if (options.pagination && options.pagination.querySize >= DEFAULT_MAX_TABLE_QUERY_SIZE) { + throw new Error(`No query size above ${DEFAULT_MAX_TABLE_QUERY_SIZE}`); + } + return buildActionResultsQuery(options); + }, + parse: async ( + options: ActionResultsRequestOptions, + response: IEsSearchResponse + ): Promise => { + const { activePage } = options.pagination; + const inspect = { + dsl: [inspectStringifyObject(buildActionResultsQuery(options))], + }; + + return { + ...response, + inspect, + edges: response.rawResponse.hits.hits, + totalCount: response.rawResponse.hits.total, + pageInfo: { + activePage: activePage ?? 0, + fakeTotalCount: 0, + showMorePagesIndicator: false, + }, + }; + }, +}; diff --git a/x-pack/plugins/osquery/server/search_strategy/osquery/factory/actions/results/query.action_results.dsl.ts b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/actions/results/query.action_results.dsl.ts new file mode 100644 index 0000000000000..8b80427d4d0e0 --- /dev/null +++ b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/actions/results/query.action_results.dsl.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 { ISearchRequestParams } from '../../../../../../../../../src/plugins/data/common'; +import { ActionResultsRequestOptions } from '../../../../../../common/search_strategy'; +import { createQueryFilterClauses } from '../../../../../../common/utils/build_query'; + +export const buildActionResultsQuery = ({ + actionId, + docValueFields, + filterQuery, + pagination: { activePage, querySize }, + sort, +}: ActionResultsRequestOptions): ISearchRequestParams => { + const filter = [ + ...createQueryFilterClauses(filterQuery), + { + match_phrase: { + action_id: actionId, + }, + }, + ]; + + const dslQuery = { + allowNoIndices: true, + index: '.fleet-actions-results', + ignoreUnavailable: true, + body: { + query: { bool: { filter } }, + from: activePage * querySize, + size: querySize, + track_total_hits: true, + fields: ['*'], + }, + }; + + return dslQuery; +}; diff --git a/x-pack/plugins/osquery/server/search_strategy/osquery/factory/agents/index.ts b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/agents/index.ts new file mode 100644 index 0000000000000..615343c738d78 --- /dev/null +++ b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/agents/index.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 { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common'; +import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../../../common/constants'; +import { + AgentsStrategyResponse, + AgentsRequestOptions, + OsqueryQueries, +} from '../../../../../common/search_strategy/osquery'; + +import { Agent } from '../../../../../common/shared_imports'; +import { inspectStringifyObject } from '../../../../../common/utils/build_query'; +import { OsqueryFactory } from '../types'; +import { buildAgentsQuery } from './query.all_agents.dsl'; + +export const allAgents: OsqueryFactory = { + buildDsl: (options: AgentsRequestOptions) => { + if (options.pagination && options.pagination.querySize >= DEFAULT_MAX_TABLE_QUERY_SIZE) { + throw new Error(`No query size above ${DEFAULT_MAX_TABLE_QUERY_SIZE}`); + } + return buildAgentsQuery(options); + }, + parse: async ( + options: AgentsRequestOptions, + response: IEsSearchResponse + ): Promise => { + const { activePage } = options.pagination; + const inspect = { + dsl: [inspectStringifyObject(buildAgentsQuery(options))], + }; + + return { + ...response, + inspect, + edges: response.rawResponse.hits.hits.map((hit) => ({ _id: hit._id, ...hit._source })), + totalCount: response.rawResponse.hits.total, + pageInfo: { + activePage: activePage ?? 0, + fakeTotalCount: 0, + showMorePagesIndicator: false, + }, + }; + }, +}; diff --git a/x-pack/plugins/osquery/server/search_strategy/osquery/factory/agents/query.all_agents.dsl.ts b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/agents/query.all_agents.dsl.ts new file mode 100644 index 0000000000000..935a6cd7b215e --- /dev/null +++ b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/agents/query.all_agents.dsl.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 { isEmpty } from 'lodash/fp'; +import { ISearchRequestParams } from '../../../../../../../../src/plugins/data/common'; +import { AgentsRequestOptions } from '../../../../../common/search_strategy'; +import { createQueryFilterClauses } from '../../../../../common/utils/build_query'; + +export const buildAgentsQuery = ({ + docValueFields, + filterQuery, + pagination: { querySize }, + sort, +}: AgentsRequestOptions): ISearchRequestParams => { + const filter = [...createQueryFilterClauses(filterQuery)]; + + const dslQuery = { + allowNoIndices: true, + index: '.fleet-agents', + ignoreUnavailable: true, + body: { + ...(!isEmpty(docValueFields) ? { docvalue_fields: docValueFields } : {}), + query: { bool: { filter } }, + track_total_hits: true, + }, + }; + + return dslQuery; +}; diff --git a/x-pack/plugins/osquery/server/search_strategy/osquery/factory/index.ts b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/index.ts new file mode 100644 index 0000000000000..dc2e741dbe3e4 --- /dev/null +++ b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/index.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 { FactoryQueryTypes, OsqueryQueries } from '../../../../common/search_strategy/osquery'; + +import { allActions, actionDetails, actionResults } from './actions'; +import { allAgents } from './agents'; +import { allResults } from './results'; + +import { OsqueryFactory } from './types'; + +export const osqueryFactory: Record> = { + [OsqueryQueries.actions]: allActions, + [OsqueryQueries.actionDetails]: actionDetails, + [OsqueryQueries.actionResults]: actionResults, + [OsqueryQueries.agents]: allAgents, + [OsqueryQueries.results]: allResults, +}; diff --git a/x-pack/plugins/osquery/server/search_strategy/osquery/factory/results/index.ts b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/results/index.ts new file mode 100644 index 0000000000000..1460a0e5d331e --- /dev/null +++ b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/results/index.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 { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common'; +import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../../../common/constants'; +import { + ResultsStrategyResponse, + ResultsRequestOptions, + OsqueryQueries, +} from '../../../../../common/search_strategy/osquery'; +import { inspectStringifyObject } from '../../../../../common/utils/build_query'; +import { OsqueryFactory } from '../types'; +import { buildResultsQuery } from './query.all_results.dsl'; + +export const allResults: OsqueryFactory = { + buildDsl: (options: ResultsRequestOptions) => { + if (options.pagination && options.pagination.querySize >= DEFAULT_MAX_TABLE_QUERY_SIZE) { + throw new Error(`No query size above ${DEFAULT_MAX_TABLE_QUERY_SIZE}`); + } + return buildResultsQuery(options); + }, + parse: async ( + options: ResultsRequestOptions, + response: IEsSearchResponse + ): Promise => { + const { activePage } = options.pagination; + const inspect = { + dsl: [inspectStringifyObject(buildResultsQuery(options))], + }; + + return { + ...response, + inspect, + edges: response.rawResponse.hits.hits, + totalCount: response.rawResponse.hits.total, + pageInfo: { + activePage: activePage ?? 0, + fakeTotalCount: 0, + showMorePagesIndicator: false, + }, + }; + }, +}; diff --git a/x-pack/plugins/osquery/server/search_strategy/osquery/factory/results/query.all_results.dsl.ts b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/results/query.all_results.dsl.ts new file mode 100644 index 0000000000000..c099e2762d741 --- /dev/null +++ b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/results/query.all_results.dsl.ts @@ -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 { ISearchRequestParams } from '../../../../../../../../src/plugins/data/common'; +import { ResultsRequestOptions } from '../../../../../common/search_strategy'; +import { createQueryFilterClauses } from '../../../../../common/utils/build_query'; + +export const buildResultsQuery = ({ + actionId, + filterQuery, + pagination: { activePage, querySize }, + sort, +}: ResultsRequestOptions): ISearchRequestParams => { + const filter = [ + ...createQueryFilterClauses(filterQuery), + { + match_phrase: { + action_id: actionId, + }, + }, + ]; + + const dslQuery = { + allowNoIndices: true, + index: 'logs-elastic_agent.osquery*', + ignoreUnavailable: true, + body: { + query: { bool: { filter } }, + from: activePage * querySize, + size: querySize, + track_total_hits: true, + fields: ['agent.*', 'osquery.*'], + }, + }; + + return dslQuery; +}; diff --git a/x-pack/plugins/osquery/server/search_strategy/osquery/factory/types.ts b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/types.ts new file mode 100644 index 0000000000000..bc2bf63958a09 --- /dev/null +++ b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/types.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { + IEsSearchResponse, + ISearchRequestParams, +} from '../../../../../../../src/plugins/data/common'; +import { + FactoryQueryTypes, + StrategyRequestType, + StrategyResponseType, +} from '../../../../common/search_strategy/osquery'; + +export interface OsqueryFactory { + buildDsl: (options: StrategyRequestType) => ISearchRequestParams; + parse: ( + options: StrategyRequestType, + response: IEsSearchResponse + ) => Promise>; +} diff --git a/x-pack/plugins/osquery/server/search_strategy/osquery/index.ts b/x-pack/plugins/osquery/server/search_strategy/osquery/index.ts new file mode 100644 index 0000000000000..8d8a255f2fcdd --- /dev/null +++ b/x-pack/plugins/osquery/server/search_strategy/osquery/index.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 { map, mergeMap } from 'rxjs/operators'; +import { + ISearchStrategy, + PluginStart, + shimHitsTotal, +} from '../../../../../../src/plugins/data/server'; +import { ENHANCED_ES_SEARCH_STRATEGY } from '../../../../data_enhanced/common'; +import { + FactoryQueryTypes, + StrategyResponseType, + StrategyRequestType, +} from '../../../common/search_strategy/osquery'; +import { osqueryFactory } from './factory'; +import { OsqueryFactory } from './factory/types'; + +export const osquerySearchStrategyProvider = ( + data: PluginStart +): ISearchStrategy, StrategyResponseType> => { + const es = data.search.getSearchStrategy(ENHANCED_ES_SEARCH_STRATEGY); + + return { + search: (request, options, deps) => { + if (request.factoryQueryType == null) { + throw new Error('factoryQueryType is required'); + } + const queryFactory: OsqueryFactory = osqueryFactory[request.factoryQueryType]; + const dsl = queryFactory.buildDsl(request); + return es.search({ ...request, params: dsl }, options, deps).pipe( + map((response) => { + return { + ...response, + ...{ + rawResponse: shimHitsTotal(response.rawResponse), + }, + }; + }), + mergeMap((esSearchRes) => queryFactory.parse(request, esSearchRes)) + ); + }, + cancel: async (id, options, deps) => { + if (es.cancel) { + return es.cancel(id, options, deps); + } + }, + }; +}; diff --git a/x-pack/plugins/osquery/server/types.ts b/x-pack/plugins/osquery/server/types.ts new file mode 100644 index 0000000000000..51ef28b4c3478 --- /dev/null +++ b/x-pack/plugins/osquery/server/types.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + PluginSetup as DataPluginSetup, + PluginStart as DataPluginStart, +} from '../../../../src/plugins/data/server'; +import { FleetStartContract } from '../../fleet/server'; + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface OsqueryPluginSetup {} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface OsqueryPluginStart {} + +export interface SetupPlugins { + data: DataPluginSetup; +} + +export interface StartPlugins { + data: DataPluginStart; + fleet?: FleetStartContract; +}