[Security Solution] Init Osquery plugin (#87109) (#89527)

This commit is contained in:
Patryk Kopyciński 2021-01-28 10:33:33 +01:00 committed by GitHub
parent 06429091cf
commit 2f06b32f11
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
106 changed files with 4615 additions and 0 deletions

View file

@ -1196,6 +1196,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
*/

View file

@ -456,6 +456,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.

View file

@ -103,4 +103,5 @@ pageLoadAssetSize:
stackAlerts: 29684
presentationUtil: 28545
spacesOss: 18817
osquery: 107090
mapsFileUpload: 23775

View file

@ -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"],

View file

@ -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.

View file

@ -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';

View file

@ -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[];
}

View file

@ -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[];
}

View file

@ -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[];
}

View file

@ -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[];
}

View file

@ -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[];
}

View file

@ -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<Record<string, string>> = {
'os.platform': 'os.platform',
'os.full': 'os.full',
'os.family': 'os.family',
'os.version': 'os.version',
'os.kernel': 'os.kernel',
};
const expected: Record<string, string> = {
'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<string, string> = {
'host.id': 'host.id',
'host.ip': 'host.ip',
'host.name': 'host.name',
};
const osFieldsMap: Readonly<Record<string, string>> = {
'os.platform': 'os.platform',
'os.full': 'os.full',
'os.family': 'os.family',
'os.version': 'os.version',
'os.kernel': 'os.kernel',
};
const expected: Record<string, string> = {
'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);
});
});
});

View file

@ -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<Record<string, string>>
): Readonly<Record<string, string>> =>
Object.entries(map).reduce<Record<string, string>>((accum, [key, value]) => {
accum[`${path}.${key}`] = `${path}.${value}`;
return accum;
}, {});

View file

@ -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<Record<string, string>> = {
'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<Record<string, string>> = {
'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<Record<string, string>> = {
'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<Record<string, string>> = {
'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<Record<string, string>> = {
'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<Record<string, string>> = {
'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<Record<string, string>> = {
'agent.type': 'agent.type',
};
export const userFieldsMap: Readonly<Record<string, string>> = {
'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<Record<string, string>> = {
'winlog.event_id': 'winlog.event_id',
};
export const suricataFieldsMap: Readonly<Record<string, string>> = {
'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<Record<string, string>> = {
'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<Record<string, string>> = {
'url.original': 'url.original',
'url.domain': 'url.domain',
'user.username': 'user.username',
'user.password': 'user.password',
};
export const httpFieldsMap: Readonly<Record<string, string>> = {
'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<Record<string, string>> = {
'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<Record<string, string>> = {
'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<Record<string, string>> = {
'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<Record<string, string>> = {
'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<Record<string, string>> = {
'geo.region_name': 'destination.geo.region_name',
'geo.country_iso_code': 'destination.geo.country_iso_code',
};
export const dnsFieldsMap: Readonly<Record<string, string>> = {
'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<Record<string, string>> = {
'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<Record<string, string>> = {
'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<Record<string, string>> = {
'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<Record<string, string>> = {
'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<Record<string, string>> = {
'rule.reference': 'rule.reference',
};
export const eventFieldsMap: Readonly<Record<string, string>> = {
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 },
};

View file

@ -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[];
}

View file

@ -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[];
}

View file

@ -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;
}

View file

@ -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[];
}

View file

@ -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[];
}

View file

@ -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[];
}

View file

@ -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;
}

View file

@ -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[];
}

View file

@ -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[];
}

View file

@ -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[];
}

View file

@ -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[];
};
}

View file

@ -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[];
}

View file

@ -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[];
}

View file

@ -0,0 +1,32 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* 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[];
}

View file

@ -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;
}

View file

@ -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[];
}

View file

@ -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[];
}

View file

@ -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[];
}

View file

@ -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[];
}

View file

@ -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';

View file

@ -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> = T | null;
export type SearchHit = IEsSearchResponse<object>['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<string>;
tiebreaker?: Maybe<string>;
}
export enum Direction {
asc = 'asc',
desc = 'desc',
}
export interface SortField<Field = string> {
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<string>;
/** The tiebreaker parameter allow to be more precise to fetch the next item */
tiebreaker?: Maybe<string>;
}
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<T, U> {
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;
};
};
}

View file

@ -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';

View file

@ -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<object>['hits']['hits'];
export type ActionResultEdges = SearchResponse<object>['hits']['hits'];
export interface ActionsStrategyResponse extends IEsSearchResponse {
edges: ActionEdges;
totalCount: number;
pageInfo: PageInfoPaginated;
inspect?: Maybe<Inspect>;
}
export type ActionsRequestOptions = RequestOptionsPaginated<{}>;
export interface ActionDetailsStrategyResponse extends IEsSearchResponse {
actionDetails: Record<string, any>;
inspect?: Maybe<Inspect>;
}
export interface ActionDetailsRequestOptions extends RequestOptions {
actionId: string;
}
export interface ActionResultsStrategyResponse extends IEsSearchResponse {
edges: ActionResultEdges;
totalCount: number;
pageInfo: PageInfoPaginated;
inspect?: Maybe<Inspect>;
}
export interface ActionResultsRequestOptions extends RequestOptionsPaginated {
actionId: string;
}

View file

@ -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<Inspect>;
}
export type AgentsRequestOptions = RequestOptionsPaginated<{}>;

View file

@ -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<string>;
sensorVersion?: Maybe<string>;
policyStatus?: Maybe<HostPolicyResponseActionStatus>;
}
export interface HostItem {
_id?: Maybe<string>;
cloud?: Maybe<CloudEcs>;
endpoint?: Maybe<EndpointFields>;
host?: Maybe<HostEcs>;
lastSeen?: Maybe<string>;
}
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<OsEcs> } };
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<number, HostHit>;

View file

@ -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<Field = string> extends RequestBasicOptions {
pagination: PaginationInputPaginated;
sort: SortField<Field>;
}
export type StrategyResponseType<T extends FactoryQueryTypes> = 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 FactoryQueryTypes> = 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;

View file

@ -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<unknown>['hits']['hits'];
export interface ResultsStrategyResponse extends IEsSearchResponse {
edges: ResultEdges;
totalCount: number;
pageInfo: PageInfoPaginated;
inspect?: Maybe<Inspect>;
}
export interface ResultsRequestOptions extends RequestOptionsPaginated<{}> {
actionId: string;
}

View file

@ -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';

View file

@ -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<string, string>;
}
export interface ESBoolQuery {
bool: {
must: DslQuery[];
filter: Filter[];
should: never[];
must_not: Filter[];
};
}

View file

@ -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<ReactNode>;
description: NonNullable<ReactNode>;
}
export const unionWithNullType = <T extends runtimeTypes.Mixed>(type: T) =>
runtimeTypes.union([type, runtimeTypes.null]);
export const stringEnum = <T>(enumObj: T, enumName = 'enum') =>
new runtimeTypes.Type<T[keyof T], string>(
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}`);
};

View file

@ -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] : [];

View file

@ -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.';
}
};

View file

@ -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: ['<rootDir>/x-pack/plugins/osquery'],
};

View file

@ -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"
}

View file

@ -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<ResultEdges>([]);
interface ActionResultsTableProps {
actionId: string;
}
const ActionResultsTableComponent: React.FC<ActionResultsTableProps> = ({ 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<EuiDataGridColumn[]>([]);
// ** Sorting config
const [sortingColumns, setSortingColumns] = useState<EuiDataGridSorting['columns']>([]);
const [, { results, totalCount }] = useAllResults({
actionId,
activePage: pagination.pageIndex,
limit: pagination.pageSize,
direction: Direction.asc,
sortField: '@timestamp',
});
// Column visibility
const [visibleColumns, setVisibleColumns] = useState<string[]>([]); // 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 (
<DataContext.Provider value={results}>
<EuiDataGrid
aria-label="Osquery results"
columns={columns}
columnVisibility={columnVisibility}
rowCount={totalCount}
renderCellValue={renderCellValue}
sorting={tableSorting}
pagination={tablePagination}
/>
</DataContext.Provider>
);
};
export const ActionResultsTable = React.memo(ActionResultsTableComponent);

View file

@ -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 = <T extends FactoryQueryTypes>(
response: StrategyResponseType<T>,
prevResponse: InspectResponse
): InspectResponse => ({
dsl: response?.inspect?.dsl ?? prevResponse?.dsl ?? [],
response:
response != null ? [JSON.stringify(response.rawResponse, null, 2)] : prevResponse?.response,
});

View file

@ -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`,
});

View file

@ -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<ResultsRequestOptions | null>(null);
const [resultsResponse, setResultsResponse] = useState<ResultsArgs>({
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<ResultsRequestOptions, ResultsStrategyResponse>(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];
};

View file

@ -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<ActionEdges>([]);
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<EuiDataGridColumn[]>([]);
// ** Sorting config
const [sortingColumns, setSortingColumns] = useState<EuiDataGridSorting['columns']>([]);
const [, { actions, totalCount }] = useAllActions({
activePage: pagination.pageIndex,
limit: pagination.pageSize,
direction: Direction.asc,
sortField: '@timestamp',
});
// Column visibility
const [visibleColumns, setVisibleColumns] = useState<string[]>([]); // 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 (
<DataContext.Provider value={actions}>
<EuiDataGrid
aria-label="Osquery actions"
columns={columns}
columnVisibility={columnVisibility}
rowCount={totalCount}
renderCellValue={renderCellValue}
sorting={tableSorting}
pagination={tablePagination}
/>
</DataContext.Provider>
);
};
export const ActionsTable = React.memo(ActionsTableComponent);

View file

@ -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 = <T extends FactoryQueryTypes>(
response: StrategyResponseType<T>,
prevResponse: InspectResponse
): InspectResponse => ({
dsl: response?.inspect?.dsl ?? prevResponse?.dsl ?? [],
response:
response != null ? [JSON.stringify(response.rawResponse, null, 2)] : prevResponse?.response,
});

View file

@ -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`,
}
);

View file

@ -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<string, string>;
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<ActionDetailsRequestOptions | null>(null);
const [actionDetailsResponse, setActionDetailsResponse] = useState<ActionDetailsArgs>({
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<ActionDetailsRequestOptions, ActionDetailsStrategyResponse>(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];
};

View file

@ -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<ActionsRequestOptions | null>(null);
const [actionsResponse, setActionsResponse] = useState<ActionsArgs>({
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<ActionsRequestOptions, ActionsStrategyResponse>(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];
};

View file

@ -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<AgentsTableProps> = ({ selectedAgents, onChange }) => {
const [pageIndex, setPageIndex] = useState(0);
const [pageSize, setPageSize] = useState(5);
const [sortField, setSortField] = useState<keyof Agent>('id');
const [sortDirection, setSortDirection] = useState<Direction>(Direction.asc);
const [selectedItems, setSelectedItems] = useState([]);
const tableRef = useRef<EuiBasicTable<Agent>>(null);
const onTableChange: EuiBasicTableProps<Agent>['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 <EuiHealth color={color}>{label}</EuiHealth>;
};
const [, { agents, totalCount }] = useAllAgents({
activePage: pageIndex,
limit: pageSize,
direction: sortDirection,
sortField,
});
const columns: Array<EuiBasicTableColumn<{}>> = 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<Agent>['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 (
<EuiBasicTable<Agent>
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);

View file

@ -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 = <T extends FactoryQueryTypes>(
response: StrategyResponseType<T>,
prevResponse: InspectResponse
): InspectResponse => ({
dsl: response?.inspect?.dsl ?? prevResponse?.dsl ?? [],
response:
response != null ? [JSON.stringify(response.rawResponse, null, 2)] : prevResponse?.response,
});

View file

@ -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`,
});

View file

@ -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<AgentsRequestOptions | null>(null);
const [agentsResponse, setAgentsResponse] = useState<AgentsArgs>({
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<AgentsRequestOptions, AgentsStrategyResponse>(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];
};

View file

@ -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$<boolean>(DEFAULT_DARK_MODE);
const theme = useMemo(
() => ({
eui: darkMode ? euiDarkVars : euiLightVars,
darkMode,
}),
[darkMode]
);
return (
<ThemeProvider theme={theme}>
<OsqueryApp />
</ThemeProvider>
);
};
export const renderApp = (
core: CoreStart,
services: AppPluginStartDependencies,
{ element, history }: AppMountParameters,
storage: Storage,
kibanaVersion: string
) => {
ReactDOM.render(
<KibanaContextProvider
// eslint-disable-next-line react-perf/jsx-no-new-object-as-prop
services={{
appName: PLUGIN_NAME,
...core,
...services,
storage,
}}
>
<EuiErrorBoundary>
<Router history={history}>
<I18nProvider>
<OsqueryAppContext />
</I18nProvider>
</Router>
</EuiErrorBoundary>
</KibanaContextProvider>,
element
);
return () => ReactDOM.unmountComponentAtNode(element);
};

View file

@ -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);
});
});
});

View file

@ -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);

View file

@ -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';

View file

@ -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';

View file

@ -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<StartServices>;
export interface WithKibanaProps {
kibana: KibanaContext;
}
const useTypedKibana = () => useKibana<StartServices>();
export {
KibanaContextProvider,
useTypedKibana as useKibana,
useUiSetting,
useUiSetting$,
withKibana,
};

View file

@ -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 (
<EuiPage restrictWidth="1000px">
<EuiPageBody>
<EuiPageHeader>
<EuiTitle size="l">
<h1>
<FormattedMessage
id="xpack.osquery.helloWorldText"
defaultMessage="{name}"
// eslint-disable-next-line react-perf/jsx-no-new-object-as-prop
values={{ name: PLUGIN_NAME }}
/>
</h1>
</EuiTitle>
</EuiPageHeader>
<EuiPageContent>
<EuiPageContentBody>
<EuiSpacer />
<Switch>
<Route path={`/live_query`}>
<LiveQuery />
</Route>
</Switch>
<EuiSpacer />
</EuiPageContentBody>
</EuiPageContent>
</EuiPageBody>
</EuiPage>
);
};
export const OsqueryApp = React.memo(OsqueryAppComponent);

View file

@ -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<OsqueryEditorProps> = ({ defaultValue, onChange }) => {
const handleChange = useCallback(
(newValue) => {
onChange(newValue);
},
[onChange]
);
return (
<EuiCodeEditor
value={defaultValue}
mode="sql"
theme="tomorrow"
onChange={handleChange}
name="osquery_editor"
setOptions={EDITOR_SET_OPTIONS}
editorProps={EDITOR_PROPS}
height="200px"
/>
);
};
export const OsqueryEditor = React.memo(OsqueryEditorComponent);

View file

@ -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';

View file

@ -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) && (
<LiveQueryForm actionDetails={actionDetails} onSubmit={handleSubmit} />
)}
<EuiSpacer />
<ResultTabs />
</>
);
};
export const EditLiveQueryPage = React.memo(EditLiveQueryPageComponent);

View file

@ -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: (
<>
<EuiSpacer />
<ActionResultsTable actionId={actionId} />
</>
),
},
{
id: 'results',
name: 'Results',
content: (
<>
<EuiSpacer />
<ResultsTable actionId={actionId} />
</>
),
},
],
[actionId]
);
const handleTabClick = useCallback((tab) => {
// eslint-disable-next-line no-console
console.log('clicked tab', tab);
}, []);
return (
<EuiTabbedContent
tabs={tabs}
initialSelectedTab={tabs[0]}
autoFocus="selected"
onTabClick={handleTabClick}
/>
);
};
export const ResultTabs = React.memo(ResultTabsComponent);

View file

@ -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<string[]>;
}
const AgentsTableFieldComponent: React.FC<AgentsTableFieldProps> = ({ field }) => {
const { value, setValue } = field;
const handleChange = useCallback(
(props) => {
if (props !== value) {
return setValue(props);
}
},
[value, setValue]
);
return <AgentsTable selectedAgents={value} onChange={handleChange} />;
};
export const AgentsTableField = React.memo(AgentsTableFieldComponent);

View file

@ -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<CodeEditorFieldProps> = ({ field }) => {
const { value, setValue } = field;
const handleChange = useCallback(
(newQuery) => {
setValue({
...value,
query: newQuery,
});
},
[value, setValue]
);
return <OsqueryEditor defaultValue={value.query} onChange={handleChange} />;
};
export const CodeEditorField = React.memo(CodeEditorFieldComponent);

View file

@ -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<string, string>;
onSubmit: (payload: Record<string, string>) => Promise<void>;
}
const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({ 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 (
<Form form={form}>
<UseField path="agents" component={AgentsTableField} />
<EuiSpacer />
<UseField path="command" component={CodeEditorField} />
<EuiButton onClick={submit}>Send query</EuiButton>
</Form>
);
};
export const LiveQueryForm = React.memo(LiveQueryFormComponent);

View file

@ -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: [],
},
};

View file

@ -0,0 +1,32 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* 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 (
<Switch>
<Route path={`${match.url}/queries/new`}>
<NewLiveQueryPage />
</Route>
<Route path={`${match.url}/queries/:actionId`}>
<EditLiveQueryPage />
</Route>
<Route path={`${match.url}/queries`}>
<QueriesPage />
</Route>
</Switch>
);
};
export const LiveQuery = React.memo(LiveQueryComponent);

View file

@ -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 <LiveQueryForm onSubmit={handleSubmit} />;
};
export const NewLiveQueryPage = React.memo(NewLiveQueryPageComponent);

View file

@ -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 (
<>
<EuiTitle>
<h1>{'Queries'}</h1>
</EuiTitle>
<EuiSpacer />
<ActionsTable />
</>
);
};
export const QueriesPage = React.memo(QueriesPageComponent);

View file

@ -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<OsqueryPluginSetup, OsqueryPluginStart> {
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() {}
}

View file

@ -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 = <T extends FactoryQueryTypes>(
response: StrategyResponseType<T>,
prevResponse: InspectResponse
): InspectResponse => ({
dsl: response?.inspect?.dsl ?? prevResponse?.dsl ?? [],
response:
response != null ? [JSON.stringify(response.rawResponse, null, 2)] : prevResponse?.response,
});

View file

@ -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<ResultEdges>([]);
interface ResultsTableComponentProps {
actionId: string;
}
const ResultsTableComponent: React.FC<ResultsTableComponentProps> = ({ 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<EuiDataGridColumn[]>([]);
// ** Sorting config
const [sortingColumns, setSortingColumns] = useState<EuiDataGridSorting['columns']>([]);
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<string[]>([]);
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 (
<DataContext.Provider value={results}>
<EuiDataGrid
aria-label="Osquery results"
columns={columns}
columnVisibility={columnVisibility}
rowCount={totalCount}
renderCellValue={renderCellValue}
sorting={tableSorting}
pagination={tablePagination}
/>
</DataContext.Provider>
);
};
export const ResultsTable = React.memo(ResultsTableComponent);

View file

@ -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`,
});

View file

@ -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<ResultsRequestOptions | null>(null);
const [resultsResponse, setResultsResponse] = useState<ResultsArgs>({
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<ResultsRequestOptions, ResultsStrategyResponse>(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];
};

View file

@ -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';

View file

@ -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;

View file

@ -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<typeof ConfigSchema>;

View file

@ -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<Readonly<ConfigType>> => {
return context.config.create<ConfigType>().pipe(map((config) => config));
};

View file

@ -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';

View file

@ -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<OsqueryPluginSetup, OsqueryPluginStart> {
private readonly logger: Logger;
constructor(private readonly initializerContext: PluginInitializerContext) {
this.logger = this.initializerContext.logger.get();
}
public async setup(core: CoreSetup<StartPlugins, OsqueryPluginStart>, 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() {}
}

View file

@ -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,
});
}
);
}

View file

@ -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<OsqueryQueries.actions> = {
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<object>
): Promise<ActionsStrategyResponse> => {
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,
},
};
},
};

View file

@ -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;
};

View file

@ -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<OsqueryQueries.actionDetails> = {
buildDsl: (options: ActionDetailsRequestOptions) => {
return buildActionDetailsQuery(options);
},
parse: async (
options: ActionDetailsRequestOptions,
response: IEsSearchResponse<unknown>
): Promise<ActionDetailsStrategyResponse> => {
const inspect = {
dsl: [inspectStringifyObject(buildActionDetailsQuery(options))],
};
return {
...response,
inspect,
actionDetails: response.rawResponse.hits.hits[0],
};
},
};

View file

@ -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;
};

View file

@ -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';

View file

@ -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<OsqueryQueries.actionResults> = {
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<object>
): Promise<ActionResultsStrategyResponse> => {
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,
},
};
},
};

View file

@ -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;
};

View file

@ -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<OsqueryQueries.agents> = {
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<Agent>
): Promise<AgentsStrategyResponse> => {
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,
},
};
},
};

View file

@ -0,0 +1,32 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* 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;
};

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