mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Asset Management] Add support for Live queries in Osquery (#89889)
This commit is contained in:
parent
a438810673
commit
ed2a1bfb1f
146 changed files with 7307 additions and 793 deletions
|
@ -1288,14 +1288,19 @@ module.exports = {
|
|||
* Osquery overrides
|
||||
*/
|
||||
{
|
||||
extends: ['eslint:recommended', 'plugin:react/recommended'],
|
||||
plugins: ['react'],
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:react/recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
],
|
||||
plugins: ['react', '@typescript-eslint'],
|
||||
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-eslint/explicit-module-boundary-types': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
"children": [],
|
||||
"source": {
|
||||
"path": "x-pack/plugins/osquery/public/types.ts",
|
||||
"lineNumber": 14
|
||||
"lineNumber": 18
|
||||
},
|
||||
"lifecycle": "setup",
|
||||
"initialIsOpen": true
|
||||
|
@ -30,7 +30,7 @@
|
|||
"children": [],
|
||||
"source": {
|
||||
"path": "x-pack/plugins/osquery/public/types.ts",
|
||||
"lineNumber": 16
|
||||
"lineNumber": 20
|
||||
},
|
||||
"lifecycle": "start",
|
||||
"initialIsOpen": true
|
||||
|
@ -52,7 +52,7 @@
|
|||
"children": [],
|
||||
"source": {
|
||||
"path": "x-pack/plugins/osquery/server/types.ts",
|
||||
"lineNumber": 15
|
||||
"lineNumber": 16
|
||||
},
|
||||
"lifecycle": "setup",
|
||||
"initialIsOpen": true
|
||||
|
@ -66,7 +66,7 @@
|
|||
"children": [],
|
||||
"source": {
|
||||
"path": "x-pack/plugins/osquery/server/types.ts",
|
||||
"lineNumber": 17
|
||||
"lineNumber": 18
|
||||
},
|
||||
"lifecycle": "start",
|
||||
"initialIsOpen": true
|
||||
|
@ -134,7 +134,7 @@
|
|||
"lineNumber": 11
|
||||
},
|
||||
"signature": [
|
||||
"\"osquery\""
|
||||
"\"Osquery\""
|
||||
],
|
||||
"initialIsOpen": false
|
||||
}
|
||||
|
|
|
@ -280,6 +280,7 @@
|
|||
"react-intl": "^2.8.0",
|
||||
"react-is": "^16.8.0",
|
||||
"react-moment-proptypes": "^1.7.0",
|
||||
"react-query": "^3.12.0",
|
||||
"react-redux": "^7.2.0",
|
||||
"react-resizable": "^1.7.5",
|
||||
"react-router": "^5.2.0",
|
||||
|
|
177
x-pack/plugins/osquery/common/exact_check.test.ts
Normal file
177
x-pack/plugins/osquery/common/exact_check.test.ts
Normal file
|
@ -0,0 +1,177 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import * as t from 'io-ts';
|
||||
import { left, right, Either } from 'fp-ts/lib/Either';
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
|
||||
import { exactCheck, findDifferencesRecursive } from './exact_check';
|
||||
import { foldLeftRight, getPaths } from './test_utils';
|
||||
|
||||
describe('exact_check', () => {
|
||||
test('it returns an error if given extra object properties', () => {
|
||||
const someType = t.exact(
|
||||
t.type({
|
||||
a: t.string,
|
||||
})
|
||||
);
|
||||
const payload = { a: 'test', b: 'test' };
|
||||
const decoded = someType.decode(payload);
|
||||
const checked = exactCheck(payload, decoded);
|
||||
const message = pipe(checked, foldLeftRight);
|
||||
expect(getPaths(left(message.errors))).toEqual(['invalid keys "b"']);
|
||||
expect(message.schema).toEqual({});
|
||||
});
|
||||
|
||||
test('it returns an error if the data type is not as expected', () => {
|
||||
type UnsafeCastForTest = Either<
|
||||
t.Errors,
|
||||
{
|
||||
a: number;
|
||||
}
|
||||
>;
|
||||
|
||||
const someType = t.exact(
|
||||
t.type({
|
||||
a: t.string,
|
||||
})
|
||||
);
|
||||
|
||||
const payload = { a: 1 };
|
||||
const decoded = someType.decode(payload);
|
||||
const checked = exactCheck(payload, decoded as UnsafeCastForTest);
|
||||
const message = pipe(checked, foldLeftRight);
|
||||
expect(getPaths(left(message.errors))).toEqual(['Invalid value "1" supplied to "a"']);
|
||||
expect(message.schema).toEqual({});
|
||||
});
|
||||
|
||||
test('it does NOT return an error if given normal object properties', () => {
|
||||
const someType = t.exact(
|
||||
t.type({
|
||||
a: t.string,
|
||||
})
|
||||
);
|
||||
const payload = { a: 'test' };
|
||||
const decoded = someType.decode(payload);
|
||||
const checked = exactCheck(payload, decoded);
|
||||
const message = pipe(checked, foldLeftRight);
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual(payload);
|
||||
});
|
||||
|
||||
test('it will return an existing error and not validate', () => {
|
||||
const payload = { a: 'test' };
|
||||
const validationError: t.ValidationError = {
|
||||
value: 'Some existing error',
|
||||
context: [],
|
||||
message: 'some error',
|
||||
};
|
||||
const error: t.Errors = [validationError];
|
||||
const leftValue = left(error);
|
||||
const checked = exactCheck(payload, leftValue);
|
||||
const message = pipe(checked, foldLeftRight);
|
||||
expect(getPaths(left(message.errors))).toEqual(['some error']);
|
||||
expect(message.schema).toEqual({});
|
||||
});
|
||||
|
||||
test('it will work with a regular "right" payload without any decoding', () => {
|
||||
const payload = { a: 'test' };
|
||||
const rightValue = right(payload);
|
||||
const checked = exactCheck(payload, rightValue);
|
||||
const message = pipe(checked, foldLeftRight);
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual({ a: 'test' });
|
||||
});
|
||||
|
||||
test('it will work with decoding a null payload when the schema expects a null', () => {
|
||||
const someType = t.union([
|
||||
t.exact(
|
||||
t.type({
|
||||
a: t.string,
|
||||
})
|
||||
),
|
||||
t.null,
|
||||
]);
|
||||
const payload = null;
|
||||
const decoded = someType.decode(payload);
|
||||
const checked = exactCheck(payload, decoded);
|
||||
const message = pipe(checked, foldLeftRight);
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual(null);
|
||||
});
|
||||
|
||||
test('it should find no differences recursively with two empty objects', () => {
|
||||
const difference = findDifferencesRecursive({}, {});
|
||||
expect(difference).toEqual([]);
|
||||
});
|
||||
|
||||
test('it should find a single difference with two objects with different keys', () => {
|
||||
const difference = findDifferencesRecursive({ a: 1 }, { b: 1 });
|
||||
expect(difference).toEqual(['a']);
|
||||
});
|
||||
|
||||
test('it should find a two differences with two objects with multiple different keys', () => {
|
||||
const difference = findDifferencesRecursive({ a: 1, c: 1 }, { b: 1 });
|
||||
expect(difference).toEqual(['a', 'c']);
|
||||
});
|
||||
|
||||
test('it should find no differences with two objects with the same keys', () => {
|
||||
const difference = findDifferencesRecursive({ a: 1, b: 1 }, { a: 1, b: 1 });
|
||||
expect(difference).toEqual([]);
|
||||
});
|
||||
|
||||
test('it should find a difference with two deep objects with different same keys', () => {
|
||||
const difference = findDifferencesRecursive({ a: 1, b: { c: 1 } }, { a: 1, b: { d: 1 } });
|
||||
expect(difference).toEqual(['c']);
|
||||
});
|
||||
|
||||
test('it should find a difference within an array', () => {
|
||||
const difference = findDifferencesRecursive({ a: 1, b: [{ c: 1 }] }, { a: 1, b: [{ a: 1 }] });
|
||||
expect(difference).toEqual(['c']);
|
||||
});
|
||||
|
||||
test('it should find a no difference when using arrays that are identical', () => {
|
||||
const difference = findDifferencesRecursive({ a: 1, b: [{ c: 1 }] }, { a: 1, b: [{ c: 1 }] });
|
||||
expect(difference).toEqual([]);
|
||||
});
|
||||
|
||||
test('it should find differences when one has an array and the other does not', () => {
|
||||
const difference = findDifferencesRecursive({ a: 1, b: [{ c: 1 }] }, { a: 1 });
|
||||
expect(difference).toEqual(['b', '[{"c":1}]']);
|
||||
});
|
||||
|
||||
test('it should find differences when one has an deep object and the other does not', () => {
|
||||
const difference = findDifferencesRecursive({ a: 1, b: { c: 1 } }, { a: 1 });
|
||||
expect(difference).toEqual(['b', '{"c":1}']);
|
||||
});
|
||||
|
||||
test('it should find differences when one has a deep object with multiple levels and the other does not', () => {
|
||||
const difference = findDifferencesRecursive({ a: 1, b: { c: { d: 1 } } }, { a: 1 });
|
||||
expect(difference).toEqual(['b', '{"c":{"d":1}}']);
|
||||
});
|
||||
|
||||
test('it tests two deep objects as the same with no key differences', () => {
|
||||
const difference = findDifferencesRecursive(
|
||||
{ a: 1, b: { c: { d: 1 } } },
|
||||
{ a: 1, b: { c: { d: 1 } } }
|
||||
);
|
||||
expect(difference).toEqual([]);
|
||||
});
|
||||
|
||||
test('it tests two deep objects with just one deep key difference', () => {
|
||||
const difference = findDifferencesRecursive(
|
||||
{ a: 1, b: { c: { d: 1 } } },
|
||||
{ a: 1, b: { c: { e: 1 } } }
|
||||
);
|
||||
expect(difference).toEqual(['d']);
|
||||
});
|
||||
|
||||
test('it should not find any differences when the original and decoded are both null', () => {
|
||||
const difference = findDifferencesRecursive(null, null);
|
||||
expect(difference).toEqual([]);
|
||||
});
|
||||
});
|
93
x-pack/plugins/osquery/common/exact_check.ts
Normal file
93
x-pack/plugins/osquery/common/exact_check.ts
Normal file
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import * as t from 'io-ts';
|
||||
import { left, Either, fold, right } from 'fp-ts/lib/Either';
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
import { isObject, get } from 'lodash/fp';
|
||||
|
||||
/**
|
||||
* Given an original object and a decoded object this will return an error
|
||||
* if and only if the original object has additional keys that the decoded
|
||||
* object does not have. If the original decoded already has an error, then
|
||||
* this will return the error as is and not continue.
|
||||
*
|
||||
* NOTE: You MUST use t.exact(...) for this to operate correctly as your schema
|
||||
* needs to remove additional keys before the compare
|
||||
*
|
||||
* You might not need this in the future if the below issue is solved:
|
||||
* https://github.com/gcanti/io-ts/issues/322
|
||||
*
|
||||
* @param original The original to check if it has additional keys
|
||||
* @param decoded The decoded either which has either an existing error or the
|
||||
* decoded object which could have additional keys stripped from it.
|
||||
*/
|
||||
export const exactCheck = <T>(
|
||||
original: unknown,
|
||||
decoded: Either<t.Errors, T>
|
||||
): Either<t.Errors, T> => {
|
||||
const onLeft = (errors: t.Errors): Either<t.Errors, T> => left(errors);
|
||||
const onRight = (decodedValue: T): Either<t.Errors, T> => {
|
||||
const differences = findDifferencesRecursive(original, decodedValue);
|
||||
if (differences.length !== 0) {
|
||||
const validationError: t.ValidationError = {
|
||||
value: differences,
|
||||
context: [],
|
||||
message: `invalid keys "${differences.join(',')}"`,
|
||||
};
|
||||
const error: t.Errors = [validationError];
|
||||
return left(error);
|
||||
} else {
|
||||
return right(decodedValue);
|
||||
}
|
||||
};
|
||||
return pipe(decoded, fold(onLeft, onRight));
|
||||
};
|
||||
|
||||
export const findDifferencesRecursive = <T>(original: unknown, decodedValue: T): string[] => {
|
||||
if (decodedValue === null && original === null) {
|
||||
// both the decodedValue and the original are null which indicates that they are equal
|
||||
// so do not report differences
|
||||
return [];
|
||||
} else if (decodedValue == null) {
|
||||
try {
|
||||
// It is null and painful when the original contains an object or an array
|
||||
// the the decoded value does not have.
|
||||
return [JSON.stringify(original)];
|
||||
} catch (err) {
|
||||
return ['circular reference'];
|
||||
}
|
||||
} else if (typeof original !== 'object' || original == null) {
|
||||
// We are not an object or null so do not report differences
|
||||
return [];
|
||||
} else {
|
||||
const decodedKeys = Object.keys(decodedValue);
|
||||
const differences = Object.keys(original).flatMap((originalKey) => {
|
||||
const foundKey = decodedKeys.some((key) => key === originalKey);
|
||||
const topLevelKey = foundKey ? [] : [originalKey];
|
||||
// I use lodash to cheat and get an any (not going to lie ;-))
|
||||
const valueObjectOrArrayOriginal = get(originalKey, original);
|
||||
const valueObjectOrArrayDecoded = get(originalKey, decodedValue);
|
||||
if (isObject(valueObjectOrArrayOriginal)) {
|
||||
return [
|
||||
...topLevelKey,
|
||||
...findDifferencesRecursive(valueObjectOrArrayOriginal, valueObjectOrArrayDecoded),
|
||||
];
|
||||
} else if (Array.isArray(valueObjectOrArrayOriginal)) {
|
||||
return [
|
||||
...topLevelKey,
|
||||
...valueObjectOrArrayOriginal.flatMap((arrayElement, index) =>
|
||||
findDifferencesRecursive(arrayElement, get(index, valueObjectOrArrayDecoded))
|
||||
),
|
||||
];
|
||||
} else {
|
||||
return topLevelKey;
|
||||
}
|
||||
});
|
||||
return differences;
|
||||
}
|
||||
};
|
188
x-pack/plugins/osquery/common/format_errors.test.ts
Normal file
188
x-pack/plugins/osquery/common/format_errors.test.ts
Normal file
|
@ -0,0 +1,188 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import * as t from 'io-ts';
|
||||
import { formatErrors } from './format_errors';
|
||||
|
||||
describe('utils', () => {
|
||||
test('returns an empty error message string if there are no errors', () => {
|
||||
const errors: t.Errors = [];
|
||||
const output = formatErrors(errors);
|
||||
expect(output).toEqual([]);
|
||||
});
|
||||
|
||||
test('returns a single error message if given one', () => {
|
||||
const validationError: t.ValidationError = {
|
||||
value: 'Some existing error',
|
||||
context: [],
|
||||
message: 'some error',
|
||||
};
|
||||
const errors: t.Errors = [validationError];
|
||||
const output = formatErrors(errors);
|
||||
expect(output).toEqual(['some error']);
|
||||
});
|
||||
|
||||
test('returns a two error messages if given two', () => {
|
||||
const validationError1: t.ValidationError = {
|
||||
value: 'Some existing error 1',
|
||||
context: [],
|
||||
message: 'some error 1',
|
||||
};
|
||||
const validationError2: t.ValidationError = {
|
||||
value: 'Some existing error 2',
|
||||
context: [],
|
||||
message: 'some error 2',
|
||||
};
|
||||
const errors: t.Errors = [validationError1, validationError2];
|
||||
const output = formatErrors(errors);
|
||||
expect(output).toEqual(['some error 1', 'some error 2']);
|
||||
});
|
||||
|
||||
test('it filters out duplicate error messages', () => {
|
||||
const validationError1: t.ValidationError = {
|
||||
value: 'Some existing error 1',
|
||||
context: [],
|
||||
message: 'some error 1',
|
||||
};
|
||||
const validationError2: t.ValidationError = {
|
||||
value: 'Some existing error 1',
|
||||
context: [],
|
||||
message: 'some error 1',
|
||||
};
|
||||
const errors: t.Errors = [validationError1, validationError2];
|
||||
const output = formatErrors(errors);
|
||||
expect(output).toEqual(['some error 1']);
|
||||
});
|
||||
|
||||
test('will use message before context if it is set', () => {
|
||||
const context: t.Context = ([{ key: 'some string key' }] as unknown) as t.Context;
|
||||
const validationError1: t.ValidationError = {
|
||||
value: 'Some existing error 1',
|
||||
context,
|
||||
message: 'I should be used first',
|
||||
};
|
||||
const errors: t.Errors = [validationError1];
|
||||
const output = formatErrors(errors);
|
||||
expect(output).toEqual(['I should be used first']);
|
||||
});
|
||||
|
||||
test('will use context entry of a single string', () => {
|
||||
const context: t.Context = ([{ key: 'some string key' }] as unknown) as t.Context;
|
||||
const validationError1: t.ValidationError = {
|
||||
value: 'Some existing error 1',
|
||||
context,
|
||||
};
|
||||
const errors: t.Errors = [validationError1];
|
||||
const output = formatErrors(errors);
|
||||
expect(output).toEqual(['Invalid value "Some existing error 1" supplied to "some string key"']);
|
||||
});
|
||||
|
||||
test('will use two context entries of two strings', () => {
|
||||
const context: t.Context = ([
|
||||
{ key: 'some string key 1' },
|
||||
{ key: 'some string key 2' },
|
||||
] as unknown) as t.Context;
|
||||
const validationError1: t.ValidationError = {
|
||||
value: 'Some existing error 1',
|
||||
context,
|
||||
};
|
||||
const errors: t.Errors = [validationError1];
|
||||
const output = formatErrors(errors);
|
||||
expect(output).toEqual([
|
||||
'Invalid value "Some existing error 1" supplied to "some string key 1,some string key 2"',
|
||||
]);
|
||||
});
|
||||
|
||||
test('will filter out and not use any strings of numbers', () => {
|
||||
const context: t.Context = ([
|
||||
{ key: '5' },
|
||||
{ key: 'some string key 2' },
|
||||
] as unknown) as t.Context;
|
||||
const validationError1: t.ValidationError = {
|
||||
value: 'Some existing error 1',
|
||||
context,
|
||||
};
|
||||
const errors: t.Errors = [validationError1];
|
||||
const output = formatErrors(errors);
|
||||
expect(output).toEqual([
|
||||
'Invalid value "Some existing error 1" supplied to "some string key 2"',
|
||||
]);
|
||||
});
|
||||
|
||||
test('will filter out and not use null', () => {
|
||||
const context: t.Context = ([
|
||||
{ key: null },
|
||||
{ key: 'some string key 2' },
|
||||
] as unknown) as t.Context;
|
||||
const validationError1: t.ValidationError = {
|
||||
value: 'Some existing error 1',
|
||||
context,
|
||||
};
|
||||
const errors: t.Errors = [validationError1];
|
||||
const output = formatErrors(errors);
|
||||
expect(output).toEqual([
|
||||
'Invalid value "Some existing error 1" supplied to "some string key 2"',
|
||||
]);
|
||||
});
|
||||
|
||||
test('will filter out and not use empty strings', () => {
|
||||
const context: t.Context = ([
|
||||
{ key: '' },
|
||||
{ key: 'some string key 2' },
|
||||
] as unknown) as t.Context;
|
||||
const validationError1: t.ValidationError = {
|
||||
value: 'Some existing error 1',
|
||||
context,
|
||||
};
|
||||
const errors: t.Errors = [validationError1];
|
||||
const output = formatErrors(errors);
|
||||
expect(output).toEqual([
|
||||
'Invalid value "Some existing error 1" supplied to "some string key 2"',
|
||||
]);
|
||||
});
|
||||
|
||||
test('will use a name context if it cannot find a keyContext', () => {
|
||||
const context: t.Context = ([
|
||||
{ key: '' },
|
||||
{ key: '', type: { name: 'someName' } },
|
||||
] as unknown) as t.Context;
|
||||
const validationError1: t.ValidationError = {
|
||||
value: 'Some existing error 1',
|
||||
context,
|
||||
};
|
||||
const errors: t.Errors = [validationError1];
|
||||
const output = formatErrors(errors);
|
||||
expect(output).toEqual(['Invalid value "Some existing error 1" supplied to "someName"']);
|
||||
});
|
||||
|
||||
test('will return an empty string if name does not exist but type does', () => {
|
||||
const context: t.Context = ([{ key: '' }, { key: '', type: {} }] as unknown) as t.Context;
|
||||
const validationError1: t.ValidationError = {
|
||||
value: 'Some existing error 1',
|
||||
context,
|
||||
};
|
||||
const errors: t.Errors = [validationError1];
|
||||
const output = formatErrors(errors);
|
||||
expect(output).toEqual(['Invalid value "Some existing error 1" supplied to ""']);
|
||||
});
|
||||
|
||||
test('will stringify an error value', () => {
|
||||
const context: t.Context = ([
|
||||
{ key: '' },
|
||||
{ key: 'some string key 2' },
|
||||
] as unknown) as t.Context;
|
||||
const validationError1: t.ValidationError = {
|
||||
value: { foo: 'some error' },
|
||||
context,
|
||||
};
|
||||
const errors: t.Errors = [validationError1];
|
||||
const output = formatErrors(errors);
|
||||
expect(output).toEqual([
|
||||
'Invalid value "{"foo":"some error"}" supplied to "some string key 2"',
|
||||
]);
|
||||
});
|
||||
});
|
32
x-pack/plugins/osquery/common/format_errors.ts
Normal file
32
x-pack/plugins/osquery/common/format_errors.ts
Normal 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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import * as t from 'io-ts';
|
||||
import { isObject } from 'lodash/fp';
|
||||
|
||||
export const formatErrors = (errors: t.Errors): string[] => {
|
||||
const err = errors.map((error) => {
|
||||
if (error.message != null) {
|
||||
return error.message;
|
||||
} else {
|
||||
const keyContext = error.context
|
||||
.filter(
|
||||
(entry) => entry.key != null && !Number.isInteger(+entry.key) && entry.key.trim() !== ''
|
||||
)
|
||||
.map((entry) => entry.key)
|
||||
.join(',');
|
||||
|
||||
const nameContext = error.context.find((entry) => entry.type?.name?.length > 0);
|
||||
const suppliedValue =
|
||||
keyContext !== '' ? keyContext : nameContext != null ? nameContext.type.name : '';
|
||||
const value = isObject(error.value) ? JSON.stringify(error.value) : error.value;
|
||||
return `Invalid value "${value}" supplied to "${suppliedValue}"`;
|
||||
}
|
||||
});
|
||||
|
||||
return [...new Set(err)];
|
||||
};
|
|
@ -8,4 +8,4 @@
|
|||
export * from './constants';
|
||||
|
||||
export const PLUGIN_ID = 'osquery';
|
||||
export const PLUGIN_NAME = 'osquery';
|
||||
export const PLUGIN_NAME = 'Osquery';
|
||||
|
|
8
x-pack/plugins/osquery/common/schemas/common/index.ts
Normal file
8
x-pack/plugins/osquery/common/schemas/common/index.ts
Normal 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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export * from './schemas';
|
28
x-pack/plugins/osquery/common/schemas/common/schemas.ts
Normal file
28
x-pack/plugins/osquery/common/schemas/common/schemas.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import * as t from 'io-ts';
|
||||
|
||||
export const name = t.string;
|
||||
export type Name = t.TypeOf<typeof name>;
|
||||
export const nameOrUndefined = t.union([name, t.undefined]);
|
||||
export type NameOrUndefined = t.TypeOf<typeof nameOrUndefined>;
|
||||
|
||||
export const description = t.string;
|
||||
export type Description = t.TypeOf<typeof description>;
|
||||
export const descriptionOrUndefined = t.union([description, t.undefined]);
|
||||
export type DescriptionOrUndefined = t.TypeOf<typeof descriptionOrUndefined>;
|
||||
|
||||
export const platform = t.string;
|
||||
export type Platform = t.TypeOf<typeof platform>;
|
||||
export const platformOrUndefined = t.union([platform, t.undefined]);
|
||||
export type PlatformOrUndefined = t.TypeOf<typeof platformOrUndefined>;
|
||||
|
||||
export const query = t.string;
|
||||
export type Query = t.TypeOf<typeof query>;
|
||||
export const queryOrUndefined = t.union([query, t.undefined]);
|
||||
export type QueryOrUndefined = t.TypeOf<typeof queryOrUndefined>;
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import * as t from 'io-ts';
|
||||
|
||||
import { name, description, Description, platform, query } from '../../common/schemas';
|
||||
import { RequiredKeepUndefined } from '../../../types';
|
||||
|
||||
export const createSavedQueryRequestSchema = t.type({
|
||||
name,
|
||||
description,
|
||||
platform,
|
||||
query,
|
||||
});
|
||||
|
||||
export type CreateSavedQueryRequestSchema = t.OutputOf<typeof createSavedQueryRequestSchema>;
|
||||
|
||||
// This type is used after a decode since some things are defaults after a decode.
|
||||
export type CreateSavedQueryRequestSchemaDecoded = Omit<
|
||||
RequiredKeepUndefined<t.TypeOf<typeof createSavedQueryRequestSchema>>,
|
||||
'description'
|
||||
> & {
|
||||
description: Description;
|
||||
};
|
|
@ -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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export * from './create_saved_query_request_schema';
|
|
@ -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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { DefaultUuid } from './default_uuid';
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
import { left } from 'fp-ts/lib/Either';
|
||||
|
||||
import { foldLeftRight, getPaths } from '../../test_utils';
|
||||
|
||||
describe('default_uuid', () => {
|
||||
test('it should validate a regular string', () => {
|
||||
const payload = '1';
|
||||
const decoded = DefaultUuid.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual(payload);
|
||||
});
|
||||
|
||||
test('it should not validate a number', () => {
|
||||
const payload = 5;
|
||||
const decoded = DefaultUuid.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual(['Invalid value "5" supplied to "DefaultUuid"']);
|
||||
expect(message.schema).toEqual({});
|
||||
});
|
||||
|
||||
test('it should return a default of a uuid', () => {
|
||||
const payload = null;
|
||||
const decoded = DefaultUuid.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toMatch(
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i
|
||||
);
|
||||
});
|
||||
});
|
25
x-pack/plugins/osquery/common/schemas/types/default_uuid.ts
Normal file
25
x-pack/plugins/osquery/common/schemas/types/default_uuid.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import * as t from 'io-ts';
|
||||
import { Either } from 'fp-ts/lib/Either';
|
||||
import uuid from 'uuid';
|
||||
|
||||
import { NonEmptyString } from './non_empty_string';
|
||||
|
||||
/**
|
||||
* Types the DefaultUuid as:
|
||||
* - If null or undefined, then a default string uuid.v4() will be
|
||||
* created otherwise it will be checked just against an empty string
|
||||
*/
|
||||
export const DefaultUuid = new t.Type<string, string | undefined, unknown>(
|
||||
'DefaultUuid',
|
||||
t.string.is,
|
||||
(input, context): Either<t.Errors, string> =>
|
||||
input == null ? t.success(uuid.v4()) : NonEmptyString.validate(input, context),
|
||||
t.identity
|
||||
);
|
|
@ -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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { NonEmptyString } from './non_empty_string';
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
import { left } from 'fp-ts/lib/Either';
|
||||
|
||||
import { foldLeftRight, getPaths } from '../../test_utils';
|
||||
|
||||
describe('non_empty_string', () => {
|
||||
test('it should validate a regular string', () => {
|
||||
const payload = '1';
|
||||
const decoded = NonEmptyString.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([]);
|
||||
expect(message.schema).toEqual(payload);
|
||||
});
|
||||
|
||||
test('it should not validate a number', () => {
|
||||
const payload = 5;
|
||||
const decoded = NonEmptyString.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([
|
||||
'Invalid value "5" supplied to "NonEmptyString"',
|
||||
]);
|
||||
expect(message.schema).toEqual({});
|
||||
});
|
||||
|
||||
test('it should not validate an empty string', () => {
|
||||
const payload = '';
|
||||
const decoded = NonEmptyString.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([
|
||||
'Invalid value "" supplied to "NonEmptyString"',
|
||||
]);
|
||||
expect(message.schema).toEqual({});
|
||||
});
|
||||
|
||||
test('it should not validate empty spaces', () => {
|
||||
const payload = ' ';
|
||||
const decoded = NonEmptyString.decode(payload);
|
||||
const message = pipe(decoded, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual([
|
||||
'Invalid value " " supplied to "NonEmptyString"',
|
||||
]);
|
||||
expect(message.schema).toEqual({});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import * as t from 'io-ts';
|
||||
import { Either } from 'fp-ts/lib/Either';
|
||||
|
||||
/**
|
||||
* Types the NonEmptyString as:
|
||||
* - A string that is not empty
|
||||
*/
|
||||
export const NonEmptyString = new t.Type<string, string, unknown>(
|
||||
'NonEmptyString',
|
||||
t.string.is,
|
||||
(input, context): Either<t.Errors, string> => {
|
||||
if (typeof input === 'string' && input.trim() !== '') {
|
||||
return t.success(input);
|
||||
} else {
|
||||
return t.failure(input, context);
|
||||
}
|
||||
},
|
||||
t.identity
|
||||
);
|
||||
|
||||
export type NonEmptyStringC = typeof NonEmptyString;
|
|
@ -21,9 +21,10 @@ export interface ActionsStrategyResponse extends IEsSearchResponse {
|
|||
inspect?: Maybe<Inspect>;
|
||||
}
|
||||
|
||||
export type ActionsRequestOptions = RequestOptionsPaginated<{}>;
|
||||
export type ActionsRequestOptions = RequestOptionsPaginated;
|
||||
|
||||
export interface ActionDetailsStrategyResponse extends IEsSearchResponse {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
actionDetails: Record<string, any>;
|
||||
inspect?: Maybe<Inspect>;
|
||||
}
|
||||
|
|
|
@ -18,4 +18,4 @@ export interface AgentsStrategyResponse extends IEsSearchResponse {
|
|||
inspect?: Maybe<Inspect>;
|
||||
}
|
||||
|
||||
export type AgentsRequestOptions = RequestOptionsPaginated<{}>;
|
||||
export type AgentsRequestOptions = RequestOptionsPaginated;
|
||||
|
|
|
@ -20,6 +20,7 @@ export interface ResultsStrategyResponse extends IEsSearchResponse {
|
|||
inspect?: Maybe<Inspect>;
|
||||
}
|
||||
|
||||
export interface ResultsRequestOptions extends RequestOptionsPaginated<{}> {
|
||||
export interface ResultsRequestOptions extends RequestOptionsPaginated {
|
||||
actionId: string;
|
||||
agentId?: string;
|
||||
}
|
||||
|
|
51
x-pack/plugins/osquery/common/test_utils.ts
Normal file
51
x-pack/plugins/osquery/common/test_utils.ts
Normal file
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import * as t from 'io-ts';
|
||||
import { fold } from 'fp-ts/lib/Either';
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
|
||||
import { formatErrors } from './format_errors';
|
||||
|
||||
interface Message<T> {
|
||||
errors: t.Errors;
|
||||
schema: T | {};
|
||||
}
|
||||
|
||||
const onLeft = <T>(errors: t.Errors): Message<T> => {
|
||||
return { errors, schema: {} };
|
||||
};
|
||||
|
||||
const onRight = <T>(schema: T): Message<T> => {
|
||||
return {
|
||||
errors: [],
|
||||
schema,
|
||||
};
|
||||
};
|
||||
|
||||
export const foldLeftRight = fold(onLeft, onRight);
|
||||
|
||||
/**
|
||||
* Convenience utility to keep the error message handling within tests to be
|
||||
* very concise.
|
||||
* @param validation The validation to get the errors from
|
||||
*/
|
||||
export const getPaths = <A>(validation: t.Validation<A>): string[] => {
|
||||
return pipe(
|
||||
validation,
|
||||
fold(
|
||||
(errors) => formatErrors(errors),
|
||||
() => ['no errors']
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Convenience utility to remove text appended to links by EUI
|
||||
*/
|
||||
export const removeExternalLinkText = (str: string): string =>
|
||||
str.replace(/\(opens in a new tab or window\)/g, '');
|
27
x-pack/plugins/osquery/common/types.ts
Normal file
27
x-pack/plugins/osquery/common/types.ts
Normal 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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export const savedQuerySavedObjectType = 'osquery-saved-query';
|
||||
export const packSavedObjectType = 'osquery-pack';
|
||||
export type SavedObjectType = 'osquery-saved-query' | 'osquery-pack';
|
||||
|
||||
/**
|
||||
* This makes any optional property the same as Required<T> would but also has the
|
||||
* added benefit of keeping your undefined.
|
||||
*
|
||||
* For example:
|
||||
* type A = RequiredKeepUndefined<{ a?: undefined; b: number }>;
|
||||
*
|
||||
* will yield a type of:
|
||||
* type A = { a: undefined; b: number; }
|
||||
*
|
||||
*/
|
||||
export type RequiredKeepUndefined<T> = { [K in keyof T]-?: [T[K]] } extends infer U
|
||||
? U extends Record<keyof U, [unknown]>
|
||||
? { [K in keyof U]: U[K][0] }
|
||||
: never
|
||||
: never;
|
|
@ -17,10 +17,12 @@
|
|||
"kibanaReact"
|
||||
],
|
||||
"requiredPlugins": [
|
||||
"actions",
|
||||
"data",
|
||||
"dataEnhanced",
|
||||
"fleet",
|
||||
"navigation"
|
||||
"navigation",
|
||||
"triggersActionsUi"
|
||||
],
|
||||
"server": true,
|
||||
"ui": true,
|
||||
|
|
|
@ -5,12 +5,23 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
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 { find, map } from 'lodash/fp';
|
||||
import {
|
||||
EuiDataGrid,
|
||||
EuiDataGridProps,
|
||||
EuiDataGridColumn,
|
||||
EuiDataGridSorting,
|
||||
EuiHealth,
|
||||
EuiIcon,
|
||||
EuiLink,
|
||||
} from '@elastic/eui';
|
||||
import React, { createContext, useState, useCallback, useContext, useMemo } from 'react';
|
||||
|
||||
import { useAllResults } from './use_action_results';
|
||||
import { useAllAgents } from './../agents/use_all_agents';
|
||||
import { useActionResults } from './use_action_results';
|
||||
import { useAllResults } from '../results/use_all_results';
|
||||
import { Direction, ResultEdges } from '../../common/search_strategy';
|
||||
import { useRouterNavigate } from '../common/lib/kibana';
|
||||
|
||||
const DataContext = createContext<ResultEdges>([]);
|
||||
|
||||
|
@ -34,12 +45,38 @@ const ActionResultsTableComponent: React.FC<ActionResultsTableProps> = ({ action
|
|||
[setPagination]
|
||||
);
|
||||
|
||||
const [columns, setColumns] = useState<EuiDataGridColumn[]>([]);
|
||||
const [columns] = useState<EuiDataGridColumn[]>([
|
||||
{
|
||||
id: 'status',
|
||||
displayAsText: 'status',
|
||||
defaultSortDirection: Direction.asc,
|
||||
},
|
||||
{
|
||||
id: 'rows_count',
|
||||
displayAsText: '# rows',
|
||||
defaultSortDirection: Direction.asc,
|
||||
},
|
||||
{
|
||||
id: 'agent_status',
|
||||
displayAsText: 'online',
|
||||
defaultSortDirection: Direction.asc,
|
||||
},
|
||||
{
|
||||
id: 'agent',
|
||||
displayAsText: 'agent',
|
||||
defaultSortDirection: Direction.asc,
|
||||
},
|
||||
{
|
||||
id: '@timestamp',
|
||||
displayAsText: '@timestamp',
|
||||
defaultSortDirection: Direction.asc,
|
||||
},
|
||||
]);
|
||||
|
||||
// ** Sorting config
|
||||
const [sortingColumns, setSortingColumns] = useState<EuiDataGridSorting['columns']>([]);
|
||||
|
||||
const [, { results, totalCount }] = useAllResults({
|
||||
const { data: actionResultsData } = useActionResults({
|
||||
actionId,
|
||||
activePage: pagination.pageIndex,
|
||||
limit: pagination.pageSize,
|
||||
|
@ -47,23 +84,85 @@ const ActionResultsTableComponent: React.FC<ActionResultsTableProps> = ({ action
|
|||
sortField: '@timestamp',
|
||||
});
|
||||
|
||||
// Column visibility
|
||||
const [visibleColumns, setVisibleColumns] = useState<string[]>([]); // initialize to the full set of columns
|
||||
const [visibleColumns, setVisibleColumns] = useState<string[]>(() => map('id', columns)); // initialize to the full set of columns
|
||||
|
||||
const columnVisibility = useMemo(() => ({ visibleColumns, setVisibleColumns }), [
|
||||
visibleColumns,
|
||||
setVisibleColumns,
|
||||
]);
|
||||
|
||||
const { data: agentsData } = useAllAgents({
|
||||
activePage: 0,
|
||||
limit: 1000,
|
||||
direction: Direction.desc,
|
||||
sortField: 'updated_at',
|
||||
});
|
||||
|
||||
const renderCellValue: EuiDataGridProps['renderCellValue'] = useMemo(
|
||||
() => ({ rowIndex, columnId, setCellProps }) => {
|
||||
() => ({ rowIndex, columnId }) => {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const data = useContext(DataContext);
|
||||
const value = data[rowIndex].fields[columnId];
|
||||
const value = data[rowIndex];
|
||||
|
||||
return !isEmpty(value) ? value : '-';
|
||||
if (columnId === 'status') {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const linkProps = useRouterNavigate(
|
||||
`/live_query/${actionId}/results/${value.fields.agent_id[0]}`
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiIcon type="checkInCircleFilled" />
|
||||
<EuiLink {...linkProps}>{'View results'}</EuiLink>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (columnId === 'rows_count') {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const { data: allResultsData } = useAllResults({
|
||||
actionId,
|
||||
agentId: value.fields.agent_id[0],
|
||||
activePage: pagination.pageIndex,
|
||||
limit: pagination.pageSize,
|
||||
direction: Direction.asc,
|
||||
sortField: '@timestamp',
|
||||
});
|
||||
// @ts-expect-error update types
|
||||
return allResultsData?.totalCount ?? '-';
|
||||
}
|
||||
|
||||
if (columnId === 'agent_status') {
|
||||
const agentIdValue = value.fields.agent_id[0];
|
||||
// @ts-expect-error update types
|
||||
const agent = find(['_id', agentIdValue], agentsData?.agents);
|
||||
const online = agent?.active;
|
||||
const color = online ? 'success' : 'danger';
|
||||
const label = online ? 'Online' : 'Offline';
|
||||
return <EuiHealth color={color}>{label}</EuiHealth>;
|
||||
}
|
||||
|
||||
if (columnId === 'agent') {
|
||||
const agentIdValue = value.fields.agent_id[0];
|
||||
// @ts-expect-error update types
|
||||
const agent = find(['_id', agentIdValue], agentsData?.agents);
|
||||
const agentName = agent?.local_metadata.host.name;
|
||||
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const linkProps = useRouterNavigate(`/live_query/${actionId}/results/${agentIdValue}`);
|
||||
return (
|
||||
<EuiLink {...linkProps}>{`(${agent?.local_metadata.os.name}) ${agentName}`}</EuiLink>
|
||||
);
|
||||
}
|
||||
|
||||
if (columnId === '@timestamp') {
|
||||
return value.fields['@timestamp'];
|
||||
}
|
||||
|
||||
return '-';
|
||||
},
|
||||
[]
|
||||
// @ts-expect-error update types
|
||||
[actionId, agentsData?.agents, pagination.pageIndex, pagination.pageSize]
|
||||
);
|
||||
|
||||
const tableSorting: EuiDataGridSorting = useMemo(
|
||||
|
@ -81,31 +180,19 @@ const ActionResultsTableComponent: React.FC<ActionResultsTableProps> = ({ action
|
|||
[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}>
|
||||
// @ts-expect-error update types
|
||||
<DataContext.Provider value={actionResultsData?.results}>
|
||||
<EuiDataGrid
|
||||
aria-label="Osquery results"
|
||||
columns={columns}
|
||||
columnVisibility={columnVisibility}
|
||||
rowCount={totalCount}
|
||||
// @ts-expect-error update types
|
||||
rowCount={actionResultsData?.totalCount}
|
||||
renderCellValue={renderCellValue}
|
||||
sorting={tableSorting}
|
||||
pagination={tablePagination}
|
||||
height="300px"
|
||||
/>
|
||||
</DataContext.Provider>
|
||||
);
|
||||
|
|
|
@ -7,10 +7,16 @@
|
|||
|
||||
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 ERROR_ACTION_RESULTS = i18n.translate(
|
||||
'xpack.osquery.action_results.errorSearchDescription',
|
||||
{
|
||||
defaultMessage: `An error has occurred on action results search`,
|
||||
}
|
||||
);
|
||||
|
||||
export const FAIL_ALL_RESULTS = i18n.translate('xpack.osquery.results.failSearchDescription', {
|
||||
defaultMessage: `Failed to fetch results`,
|
||||
});
|
||||
export const FAIL_ACTION_RESULTS = i18n.translate(
|
||||
'xpack.osquery.action_results.failSearchDescription',
|
||||
{
|
||||
defaultMessage: `Failed to fetch action results`,
|
||||
}
|
||||
);
|
||||
|
|
|
@ -6,14 +6,14 @@
|
|||
*/
|
||||
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
|
||||
import { createFilter } from '../common/helpers';
|
||||
import { useKibana } from '../common/lib/kibana';
|
||||
import {
|
||||
ResultEdges,
|
||||
PageInfoPaginated,
|
||||
DocValueFields,
|
||||
OsqueryQueries,
|
||||
ResultsRequestOptions,
|
||||
ResultsStrategyResponse,
|
||||
|
@ -21,13 +21,8 @@ import {
|
|||
} 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;
|
||||
|
@ -37,103 +32,50 @@ export interface ResultsArgs {
|
|||
totalCount: number;
|
||||
}
|
||||
|
||||
interface UseAllResults {
|
||||
interface UseActionResults {
|
||||
actionId: string;
|
||||
activePage: number;
|
||||
direction: Direction;
|
||||
limit: number;
|
||||
sortField: string;
|
||||
docValueFields?: DocValueFields[];
|
||||
filterQuery?: ESTermQuery | string;
|
||||
skip?: boolean;
|
||||
}
|
||||
|
||||
export const useAllResults = ({
|
||||
export const useActionResults = ({
|
||||
actionId,
|
||||
activePage,
|
||||
direction,
|
||||
limit,
|
||||
sortField,
|
||||
docValueFields,
|
||||
filterQuery,
|
||||
skip = false,
|
||||
}: UseAllResults): [boolean, ResultsArgs] => {
|
||||
const { data, notifications } = useKibana().services;
|
||||
}: UseActionResults) => {
|
||||
const { data } = 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 response = useQuery(
|
||||
['actionResults', { actionId, activePage, direction, limit, sortField }],
|
||||
async () => {
|
||||
if (!resultsRequest) return Promise.resolve();
|
||||
|
||||
const resultsSearch = useCallback(
|
||||
(request: ResultsRequestOptions | null) => {
|
||||
if (request == null || skip) {
|
||||
return;
|
||||
}
|
||||
const responseData = await data.search
|
||||
.search<ResultsRequestOptions, ResultsStrategyResponse>(resultsRequest, {
|
||||
strategy: 'osquerySearchStrategy',
|
||||
})
|
||||
.toPromise();
|
||||
|
||||
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();
|
||||
return {
|
||||
...responseData,
|
||||
results: responseData.edges,
|
||||
inspect: getInspectResponse(responseData, {} as InspectResponse),
|
||||
};
|
||||
},
|
||||
[data.search, notifications.toasts, skip]
|
||||
{
|
||||
refetchInterval: 1000,
|
||||
enabled: !skip && !!resultsRequest,
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -141,7 +83,6 @@ export const useAllResults = ({
|
|||
const myRequest = {
|
||||
...(prevRequest ?? {}),
|
||||
actionId,
|
||||
docValueFields: docValueFields ?? [],
|
||||
factoryQueryType: OsqueryQueries.actionResults,
|
||||
filterQuery: createFilter(filterQuery),
|
||||
pagination: generateTablePaginationOptions(activePage, limit),
|
||||
|
@ -155,11 +96,7 @@ export const useAllResults = ({
|
|||
}
|
||||
return prevRequest;
|
||||
});
|
||||
}, [actionId, activePage, direction, docValueFields, filterQuery, limit, sortField]);
|
||||
}, [actionId, activePage, direction, filterQuery, limit, sortField]);
|
||||
|
||||
useEffect(() => {
|
||||
resultsSearch(resultsRequest);
|
||||
}, [resultsRequest, resultsSearch]);
|
||||
|
||||
return [loading, resultsResponse];
|
||||
return response;
|
||||
};
|
||||
|
|
|
@ -6,11 +6,19 @@
|
|||
*/
|
||||
|
||||
import { isEmpty, isEqual, keys, map } from 'lodash/fp';
|
||||
import { EuiDataGrid, EuiDataGridProps, EuiDataGridColumn, EuiDataGridSorting } from '@elastic/eui';
|
||||
import {
|
||||
EuiLink,
|
||||
EuiDataGrid,
|
||||
EuiDataGridProps,
|
||||
EuiDataGridColumn,
|
||||
EuiDataGridSorting,
|
||||
EuiLoadingContent,
|
||||
} 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';
|
||||
import { useRouterNavigate } from '../common/lib/kibana';
|
||||
|
||||
const DataContext = createContext<ActionEdges>([]);
|
||||
|
||||
|
@ -35,10 +43,10 @@ const ActionsTableComponent = () => {
|
|||
// ** Sorting config
|
||||
const [sortingColumns, setSortingColumns] = useState<EuiDataGridSorting['columns']>([]);
|
||||
|
||||
const [, { actions, totalCount }] = useAllActions({
|
||||
const { isLoading: actionsLoading, data: actionsData } = useAllActions({
|
||||
activePage: pagination.pageIndex,
|
||||
limit: pagination.pageSize,
|
||||
direction: Direction.asc,
|
||||
direction: Direction.desc,
|
||||
sortField: '@timestamp',
|
||||
});
|
||||
|
||||
|
@ -50,15 +58,22 @@ const ActionsTableComponent = () => {
|
|||
setVisibleColumns,
|
||||
]);
|
||||
|
||||
const renderCellValue: EuiDataGridProps['renderCellValue'] = useMemo(() => {
|
||||
return ({ rowIndex, columnId, setCellProps }) => {
|
||||
const renderCellValue: EuiDataGridProps['renderCellValue'] = useMemo(
|
||||
() => ({ rowIndex, columnId }) => {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const data = useContext(DataContext);
|
||||
const value = data[rowIndex].fields[columnId];
|
||||
|
||||
if (columnId === 'action_id') {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const linkProps = useRouterNavigate(`/live_query/${value}`);
|
||||
return <EuiLink {...linkProps}>{value}</EuiLink>;
|
||||
}
|
||||
|
||||
return !isEmpty(value) ? value : '-';
|
||||
};
|
||||
}, []);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const tableSorting: EuiDataGridSorting = useMemo(
|
||||
() => ({ columns: sortingColumns, onSort: setSortingColumns }),
|
||||
|
@ -76,7 +91,8 @@ const ActionsTableComponent = () => {
|
|||
);
|
||||
|
||||
useEffect(() => {
|
||||
const newColumns = keys(actions[0]?.fields)
|
||||
// @ts-expect-error update types
|
||||
const newColumns = keys(actionsData?.actions[0]?.fields)
|
||||
.sort()
|
||||
.map((fieldName) => ({
|
||||
id: fieldName,
|
||||
|
@ -88,15 +104,23 @@ const ActionsTableComponent = () => {
|
|||
setColumns(newColumns);
|
||||
setVisibleColumns(map('id', newColumns));
|
||||
}
|
||||
}, [columns, actions]);
|
||||
// @ts-expect-error update types
|
||||
}, [columns, actionsData?.actions]);
|
||||
|
||||
if (actionsLoading) {
|
||||
return <EuiLoadingContent lines={10} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<DataContext.Provider value={actions}>
|
||||
// @ts-expect-error update types
|
||||
// eslint-disable-next-line react-perf/jsx-no-new-array-as-prop
|
||||
<DataContext.Provider value={actionsData?.actions ?? []}>
|
||||
<EuiDataGrid
|
||||
aria-label="Osquery actions"
|
||||
columns={columns}
|
||||
columnVisibility={columnVisibility}
|
||||
rowCount={totalCount}
|
||||
// @ts-expect-error update types
|
||||
rowCount={actionsData?.totalCount ?? 0}
|
||||
renderCellValue={renderCellValue}
|
||||
sorting={tableSorting}
|
||||
pagination={tablePagination}
|
||||
|
|
|
@ -6,25 +6,20 @@
|
|||
*/
|
||||
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
|
||||
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;
|
||||
|
@ -34,88 +29,34 @@ export interface ActionDetailsArgs {
|
|||
|
||||
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;
|
||||
export const useActionDetails = ({ actionId, filterQuery, skip = false }: UseActionDetails) => {
|
||||
const { data } = 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 response = useQuery(
|
||||
['action', { actionId }],
|
||||
async () => {
|
||||
if (!actionDetailsRequest) return Promise.resolve();
|
||||
|
||||
const actionDetailsSearch = useCallback(
|
||||
(request: ActionDetailsRequestOptions | null) => {
|
||||
if (request == null || skip) {
|
||||
return;
|
||||
}
|
||||
const responseData = await data.search
|
||||
.search<ActionDetailsRequestOptions, ActionDetailsStrategyResponse>(actionDetailsRequest, {
|
||||
strategy: 'osquerySearchStrategy',
|
||||
})
|
||||
.toPromise();
|
||||
|
||||
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();
|
||||
return {
|
||||
...responseData,
|
||||
inspect: getInspectResponse(responseData, {} as InspectResponse),
|
||||
};
|
||||
},
|
||||
[data.search, notifications.toasts, skip]
|
||||
{
|
||||
enabled: !skip && !!actionDetailsRequest,
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -123,7 +64,6 @@ export const useActionDetails = ({
|
|||
const myRequest = {
|
||||
...(prevRequest ?? {}),
|
||||
actionId,
|
||||
docValueFields: docValueFields ?? [],
|
||||
factoryQueryType: OsqueryQueries.actionDetails,
|
||||
filterQuery: createFilter(filterQuery),
|
||||
};
|
||||
|
@ -132,11 +72,7 @@ export const useActionDetails = ({
|
|||
}
|
||||
return prevRequest;
|
||||
});
|
||||
}, [actionId, docValueFields, filterQuery]);
|
||||
}, [actionId, filterQuery]);
|
||||
|
||||
useEffect(() => {
|
||||
actionDetailsSearch(actionDetailsRequest);
|
||||
}, [actionDetailsRequest, actionDetailsSearch]);
|
||||
|
||||
return [loading, actionDetailsResponse];
|
||||
return response;
|
||||
};
|
||||
|
|
|
@ -5,15 +5,15 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
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,
|
||||
|
@ -21,13 +21,8 @@ import {
|
|||
} 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;
|
||||
|
@ -42,7 +37,6 @@ interface UseAllActions {
|
|||
direction: Direction;
|
||||
limit: number;
|
||||
sortField: string;
|
||||
docValueFields?: DocValueFields[];
|
||||
filterQuery?: ESTermQuery | string;
|
||||
skip?: boolean;
|
||||
}
|
||||
|
@ -52,93 +46,39 @@ export const useAllActions = ({
|
|||
direction,
|
||||
limit,
|
||||
sortField,
|
||||
docValueFields,
|
||||
filterQuery,
|
||||
skip = false,
|
||||
}: UseAllActions): [boolean, ActionsArgs] => {
|
||||
const { data, notifications } = useKibana().services;
|
||||
}: UseAllActions) => {
|
||||
const { data } = 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 response = useQuery(
|
||||
['actions', { activePage, direction, limit, sortField }],
|
||||
async () => {
|
||||
if (!actionsRequest) return Promise.resolve();
|
||||
|
||||
const actionsSearch = useCallback(
|
||||
(request: ActionsRequestOptions | null) => {
|
||||
if (request == null || skip) {
|
||||
return;
|
||||
}
|
||||
const responseData = await data.search
|
||||
.search<ActionsRequestOptions, ActionsStrategyResponse>(actionsRequest, {
|
||||
strategy: 'osquerySearchStrategy',
|
||||
})
|
||||
.toPromise();
|
||||
|
||||
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();
|
||||
return {
|
||||
...responseData,
|
||||
actions: responseData.edges,
|
||||
inspect: getInspectResponse(responseData, {} as InspectResponse),
|
||||
};
|
||||
},
|
||||
[data.search, notifications.toasts, skip]
|
||||
{
|
||||
enabled: !skip && !!actionsRequest,
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setHostRequest((prevRequest) => {
|
||||
const myRequest = {
|
||||
...(prevRequest ?? {}),
|
||||
docValueFields: docValueFields ?? [],
|
||||
factoryQueryType: OsqueryQueries.actions,
|
||||
filterQuery: createFilter(filterQuery),
|
||||
pagination: generateTablePaginationOptions(activePage, limit),
|
||||
|
@ -152,11 +92,7 @@ export const useAllActions = ({
|
|||
}
|
||||
return prevRequest;
|
||||
});
|
||||
}, [activePage, direction, docValueFields, filterQuery, limit, sortField]);
|
||||
}, [activePage, direction, filterQuery, limit, sortField]);
|
||||
|
||||
useEffect(() => {
|
||||
actionsSearch(actionsRequest);
|
||||
}, [actionsRequest, actionsSearch]);
|
||||
|
||||
return [loading, actionsResponse];
|
||||
return response;
|
||||
};
|
||||
|
|
|
@ -27,7 +27,7 @@ interface AgentsTableProps {
|
|||
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 [sortField, setSortField] = useState<keyof Agent>('upgraded_at');
|
||||
const [sortDirection, setSortDirection] = useState<Direction>(Direction.asc);
|
||||
const [selectedItems, setSelectedItems] = useState([]);
|
||||
const tableRef = useRef<EuiBasicTable<Agent>>(null);
|
||||
|
@ -49,8 +49,11 @@ const AgentsTableComponent: React.FC<AgentsTableProps> = ({ selectedAgents, onCh
|
|||
const onSelectionChange: EuiTableSelectionType<{}>['onSelectionChange'] = useCallback(
|
||||
(newSelectedItems) => {
|
||||
setSelectedItems(newSelectedItems);
|
||||
// @ts-expect-error
|
||||
onChange(newSelectedItems.map((item) => item._id));
|
||||
|
||||
if (onChange) {
|
||||
// @ts-expect-error update types
|
||||
onChange(newSelectedItems.map((item) => item._id));
|
||||
}
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
|
@ -61,7 +64,7 @@ const AgentsTableComponent: React.FC<AgentsTableProps> = ({ selectedAgents, onCh
|
|||
return <EuiHealth color={color}>{label}</EuiHealth>;
|
||||
};
|
||||
|
||||
const [, { agents, totalCount }] = useAllAgents({
|
||||
const { data = {} } = useAllAgents({
|
||||
activePage: pageIndex,
|
||||
limit: pageSize,
|
||||
direction: sortDirection,
|
||||
|
@ -96,10 +99,12 @@ const AgentsTableComponent: React.FC<AgentsTableProps> = ({ selectedAgents, onCh
|
|||
() => ({
|
||||
pageIndex,
|
||||
pageSize,
|
||||
totalItemCount: totalCount,
|
||||
// @ts-expect-error update types
|
||||
totalItemCount: data.totalCount ?? 0,
|
||||
pageSizeOptions: [3, 5, 8],
|
||||
}),
|
||||
[pageIndex, pageSize, totalCount]
|
||||
// @ts-expect-error update types
|
||||
[pageIndex, pageSize, data.totalCount]
|
||||
);
|
||||
|
||||
const sorting = useMemo(
|
||||
|
@ -123,18 +128,26 @@ const AgentsTableComponent: React.FC<AgentsTableProps> = ({ selectedAgents, onCh
|
|||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedAgents?.length && agents.length && selectedItems.length !== selectedAgents.length) {
|
||||
if (
|
||||
selectedAgents?.length &&
|
||||
// @ts-expect-error update types
|
||||
data.agents?.length &&
|
||||
selectedItems.length !== selectedAgents.length
|
||||
) {
|
||||
tableRef?.current?.setSelection(
|
||||
// @ts-expect-error
|
||||
selectedAgents.map((agentId) => find({ _id: agentId }, agents))
|
||||
// @ts-expect-error update types
|
||||
selectedAgents.map((agentId) => find({ _id: agentId }, data.agents))
|
||||
);
|
||||
}
|
||||
}, [selectedAgents, agents, selectedItems.length]);
|
||||
// @ts-expect-error update types
|
||||
}, [selectedAgents, data.agents, selectedItems.length]);
|
||||
|
||||
return (
|
||||
<EuiBasicTable<Agent>
|
||||
ref={tableRef}
|
||||
items={agents}
|
||||
// @ts-expect-error update types
|
||||
// eslint-disable-next-line react-perf/jsx-no-new-array-as-prop
|
||||
items={data.agents ?? []}
|
||||
itemId="_id"
|
||||
columns={columns}
|
||||
pagination={pagination}
|
||||
|
|
|
@ -30,9 +30,10 @@ export const generateTablePaginationOptions = (
|
|||
|
||||
export const getInspectResponse = <T extends FactoryQueryTypes>(
|
||||
response: StrategyResponseType<T>,
|
||||
prevResponse: InspectResponse
|
||||
prevResponse?: InspectResponse
|
||||
): InspectResponse => ({
|
||||
dsl: response?.inspect?.dsl ?? prevResponse?.dsl ?? [],
|
||||
// @ts-expect-error update types
|
||||
response:
|
||||
response != null ? [JSON.stringify(response.rawResponse, null, 2)] : prevResponse?.response,
|
||||
});
|
||||
|
|
|
@ -6,13 +6,13 @@
|
|||
*/
|
||||
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
|
||||
import { createFilter } from '../common/helpers';
|
||||
import { useKibana } from '../common/lib/kibana';
|
||||
import {
|
||||
PageInfoPaginated,
|
||||
DocValueFields,
|
||||
OsqueryQueries,
|
||||
AgentsRequestOptions,
|
||||
AgentsStrategyResponse,
|
||||
|
@ -21,13 +21,8 @@ import {
|
|||
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;
|
||||
|
@ -42,7 +37,6 @@ interface UseAllAgents {
|
|||
direction: Direction;
|
||||
limit: number;
|
||||
sortField: string;
|
||||
docValueFields?: DocValueFields[];
|
||||
filterQuery?: ESTermQuery | string;
|
||||
skip?: boolean;
|
||||
}
|
||||
|
@ -52,93 +46,39 @@ export const useAllAgents = ({
|
|||
direction,
|
||||
limit,
|
||||
sortField,
|
||||
docValueFields,
|
||||
filterQuery,
|
||||
skip = false,
|
||||
}: UseAllAgents): [boolean, AgentsArgs] => {
|
||||
const { data, notifications } = useKibana().services;
|
||||
}: UseAllAgents) => {
|
||||
const { data } = 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 response = useQuery(
|
||||
['agents', { activePage, direction, limit, sortField }],
|
||||
async () => {
|
||||
if (!agentsRequest) return Promise.resolve();
|
||||
|
||||
const agentsSearch = useCallback(
|
||||
(request: AgentsRequestOptions | null) => {
|
||||
if (request == null || skip) {
|
||||
return;
|
||||
}
|
||||
const responseData = await data.search
|
||||
.search<AgentsRequestOptions, AgentsStrategyResponse>(agentsRequest, {
|
||||
strategy: 'osquerySearchStrategy',
|
||||
})
|
||||
.toPromise();
|
||||
|
||||
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();
|
||||
return {
|
||||
...responseData,
|
||||
agents: responseData.edges,
|
||||
inspect: getInspectResponse(responseData),
|
||||
};
|
||||
},
|
||||
[data.search, notifications.toasts, skip]
|
||||
{
|
||||
enabled: !skip && !!agentsRequest,
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setHostRequest((prevRequest) => {
|
||||
const myRequest = {
|
||||
...(prevRequest ?? {}),
|
||||
docValueFields: docValueFields ?? [],
|
||||
factoryQueryType: OsqueryQueries.agents,
|
||||
filterQuery: createFilter(filterQuery),
|
||||
pagination: generateTablePaginationOptions(activePage, limit),
|
||||
|
@ -152,11 +92,7 @@ export const useAllAgents = ({
|
|||
}
|
||||
return prevRequest;
|
||||
});
|
||||
}, [activePage, direction, docValueFields, filterQuery, limit, sortField]);
|
||||
}, [activePage, direction, filterQuery, limit, sortField]);
|
||||
|
||||
useEffect(() => {
|
||||
agentsSearch(agentsRequest);
|
||||
}, [agentsRequest, agentsSearch]);
|
||||
|
||||
return [loading, agentsResponse];
|
||||
return response;
|
||||
};
|
||||
|
|
|
@ -13,6 +13,8 @@ import ReactDOM from 'react-dom';
|
|||
import { Router } from 'react-router-dom';
|
||||
import { I18nProvider } from '@kbn/i18n/react';
|
||||
import { ThemeProvider } from 'styled-components';
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
import { ReactQueryDevtools } from 'react-query/devtools';
|
||||
|
||||
import { useUiSetting$ } from '../../../../src/plugins/kibana_react/public';
|
||||
import { Storage } from '../../../../src/plugins/kibana_utils/public';
|
||||
|
@ -22,6 +24,8 @@ import { OsqueryApp } from './components/app';
|
|||
import { DEFAULT_DARK_MODE, PLUGIN_NAME } from '../common';
|
||||
import { KibanaContextProvider } from './common/lib/kibana';
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
const OsqueryAppContext = () => {
|
||||
const [darkMode] = useUiSetting$<boolean>(DEFAULT_DARK_MODE);
|
||||
const theme = useMemo(
|
||||
|
@ -51,6 +55,7 @@ export const renderApp = (
|
|||
// eslint-disable-next-line react-perf/jsx-no-new-object-as-prop
|
||||
services={{
|
||||
appName: PLUGIN_NAME,
|
||||
kibanaVersion,
|
||||
...core,
|
||||
...services,
|
||||
storage,
|
||||
|
@ -59,7 +64,10 @@ export const renderApp = (
|
|||
<EuiErrorBoundary>
|
||||
<Router history={history}>
|
||||
<I18nProvider>
|
||||
<OsqueryAppContext />
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<OsqueryAppContext />
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
</QueryClientProvider>
|
||||
</I18nProvider>
|
||||
</Router>
|
||||
</EuiErrorBoundary>
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import {
|
||||
KibanaContextProvider,
|
||||
KibanaReactContextValue,
|
||||
|
@ -12,6 +13,7 @@ import {
|
|||
useUiSetting,
|
||||
useUiSetting$,
|
||||
withKibana,
|
||||
reactRouterNavigate,
|
||||
} from '../../../../../../../src/plugins/kibana_react/public';
|
||||
import { StartServices } from '../../../types';
|
||||
|
||||
|
@ -22,8 +24,17 @@ export interface WithKibanaProps {
|
|||
|
||||
const useTypedKibana = () => useKibana<StartServices>();
|
||||
|
||||
const useRouterNavigate = (
|
||||
to: Parameters<typeof reactRouterNavigate>[1],
|
||||
onClickCallback?: Parameters<typeof reactRouterNavigate>[2]
|
||||
) => {
|
||||
const history = useHistory();
|
||||
return reactRouterNavigate(history, to, onClickCallback);
|
||||
};
|
||||
|
||||
export {
|
||||
KibanaContextProvider,
|
||||
useRouterNavigate,
|
||||
useTypedKibana as useKibana,
|
||||
useUiSetting,
|
||||
useUiSetting$,
|
||||
|
|
|
@ -5,54 +5,45 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { Switch, Route } from 'react-router-dom';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiTabs, EuiTab } from '@elastic/eui';
|
||||
import { useLocation } 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';
|
||||
import { Container, Nav, Wrapper } from './layouts';
|
||||
import { OsqueryAppRoutes } from '../routes';
|
||||
import { useRouterNavigate } from '../common/lib/kibana';
|
||||
|
||||
export const OsqueryAppComponent = () => {
|
||||
const location = useLocation();
|
||||
const section = useMemo(() => location.pathname.split('/')[1] ?? 'overview', [location.pathname]);
|
||||
|
||||
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>
|
||||
<Container>
|
||||
<Wrapper>
|
||||
<Nav>
|
||||
<EuiFlexGroup gutterSize="l" alignItems="center">
|
||||
<EuiFlexItem>
|
||||
<EuiTabs display="condensed">
|
||||
<EuiTab isSelected={section === 'overview'} {...useRouterNavigate('overview')}>
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.appNavigation.overviewLinkText"
|
||||
defaultMessage="Overview"
|
||||
/>
|
||||
</EuiTab>
|
||||
<EuiTab isSelected={section === 'live_query'} {...useRouterNavigate('live_query')}>
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.appNavigation.liveQueryLinkText"
|
||||
defaultMessage="Live Query"
|
||||
/>
|
||||
</EuiTab>
|
||||
</EuiTabs>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</Nav>
|
||||
<OsqueryAppRoutes />
|
||||
</Wrapper>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
34
x-pack/plugins/osquery/public/components/layouts/default.tsx
Normal file
34
x-pack/plugins/osquery/public/components/layouts/default.tsx
Normal file
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Container = styled.div`
|
||||
min-height: calc(
|
||||
100vh - ${(props) => parseFloat(props.theme.eui.euiHeaderHeightCompensation) * 2}px
|
||||
);
|
||||
background: ${(props) => props.theme.eui.euiColorEmptyShade};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
export const Wrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
`;
|
||||
|
||||
export const Nav = styled.nav`
|
||||
background: ${(props) => props.theme.eui.euiColorEmptyShade};
|
||||
border-bottom: ${(props) => props.theme.eui.euiBorderThin};
|
||||
padding: ${(props) =>
|
||||
`${props.theme.eui.euiSize} ${props.theme.eui.euiSizeL} ${props.theme.eui.euiSize} ${props.theme.eui.euiSizeL}`};
|
||||
.euiTabs {
|
||||
padding-left: 3px;
|
||||
margin-left: -3px;
|
||||
}
|
||||
`;
|
94
x-pack/plugins/osquery/public/components/layouts/header.tsx
Normal file
94
x-pack/plugins/osquery/public/components/layouts/header.tsx
Normal file
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
// copied from x-pack/plugins/fleet/public/applications/fleet/components/header.tsx
|
||||
|
||||
import React, { memo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiTabs, EuiTab, EuiSpacer } from '@elastic/eui';
|
||||
import { Props as EuiTabProps } from '@elastic/eui/src/components/tabs/tab';
|
||||
import { EuiFlexItemProps } from '@elastic/eui/src/components/flex/flex_item';
|
||||
|
||||
const Container = styled.div`
|
||||
border-bottom: ${(props) => props.theme.eui.euiBorderThin};
|
||||
background-color: ${(props) => props.theme.eui.euiPageBackgroundColor};
|
||||
`;
|
||||
|
||||
const Wrapper = styled.div<{ maxWidth?: number }>`
|
||||
max-width: ${(props) => props.maxWidth || 1200}px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding-top: ${(props) => props.theme.eui.paddingSizes.xl};
|
||||
padding-left: ${(props) => props.theme.eui.paddingSizes.m};
|
||||
padding-right: ${(props) => props.theme.eui.paddingSizes.m};
|
||||
`;
|
||||
|
||||
const Tabs = styled(EuiTabs)`
|
||||
top: 1px;
|
||||
&:before {
|
||||
height: 0px;
|
||||
}
|
||||
`;
|
||||
|
||||
export interface HeaderProps {
|
||||
maxWidth?: number;
|
||||
leftColumn?: JSX.Element;
|
||||
rightColumn?: JSX.Element;
|
||||
rightColumnGrow?: EuiFlexItemProps['grow'];
|
||||
tabs?: Array<Omit<EuiTabProps, 'name'> & { name?: JSX.Element | string }>;
|
||||
tabsClassName?: string;
|
||||
'data-test-subj'?: string;
|
||||
}
|
||||
|
||||
const HeaderColumns: React.FC<Omit<HeaderProps, 'tabs'>> = memo(
|
||||
({ leftColumn, rightColumn, rightColumnGrow }) => (
|
||||
<EuiFlexGroup alignItems="center">
|
||||
{leftColumn ? <EuiFlexItem>{leftColumn}</EuiFlexItem> : null}
|
||||
{rightColumn ? <EuiFlexItem grow={rightColumnGrow}>{rightColumn}</EuiFlexItem> : null}
|
||||
</EuiFlexGroup>
|
||||
)
|
||||
);
|
||||
|
||||
HeaderColumns.displayName = 'HeaderColumns';
|
||||
|
||||
export const Header: React.FC<HeaderProps> = ({
|
||||
leftColumn,
|
||||
rightColumn,
|
||||
rightColumnGrow,
|
||||
tabs,
|
||||
maxWidth,
|
||||
tabsClassName,
|
||||
'data-test-subj': dataTestSubj,
|
||||
}) => (
|
||||
<Container data-test-subj={dataTestSubj}>
|
||||
<Wrapper maxWidth={maxWidth}>
|
||||
<HeaderColumns
|
||||
leftColumn={leftColumn}
|
||||
rightColumn={rightColumn}
|
||||
rightColumnGrow={rightColumnGrow}
|
||||
/>
|
||||
<EuiFlexGroup>
|
||||
{tabs ? (
|
||||
<EuiFlexItem>
|
||||
<EuiSpacer size="s" />
|
||||
<Tabs className={tabsClassName}>
|
||||
{tabs.map((props) => (
|
||||
<EuiTab {...(props as EuiTabProps)} key={props.id}>
|
||||
{props.name}
|
||||
</EuiTab>
|
||||
))}
|
||||
</Tabs>
|
||||
</EuiFlexItem>
|
||||
) : (
|
||||
<EuiFlexItem>
|
||||
<EuiSpacer size="l" />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</Wrapper>
|
||||
</Container>
|
||||
);
|
12
x-pack/plugins/osquery/public/components/layouts/index.tsx
Normal file
12
x-pack/plugins/osquery/public/components/layouts/index.tsx
Normal 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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
// copied from x-pack/plugins/fleet/public/applications/fleet/layouts/index.tsx
|
||||
|
||||
export { Container, Nav, Wrapper } from './default';
|
||||
export { WithHeaderLayout, WithHeaderLayoutProps } from './with_header';
|
||||
export { WithoutHeaderLayout } from './without_header';
|
|
@ -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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { Fragment } from 'react';
|
||||
import { EuiPageBody, EuiSpacer } from '@elastic/eui';
|
||||
|
||||
import { Header, HeaderProps } from './header';
|
||||
import { Page, ContentWrapper } from './without_header';
|
||||
|
||||
export interface WithHeaderLayoutProps extends HeaderProps {
|
||||
restrictWidth?: number;
|
||||
restrictHeaderWidth?: number;
|
||||
'data-test-subj'?: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const WithHeaderLayout: React.FC<WithHeaderLayoutProps> = ({
|
||||
restrictWidth,
|
||||
restrictHeaderWidth,
|
||||
children,
|
||||
'data-test-subj': dataTestSubj,
|
||||
...rest
|
||||
}) => (
|
||||
<Fragment>
|
||||
<Header
|
||||
maxWidth={restrictHeaderWidth}
|
||||
data-test-subj={dataTestSubj ? `${dataTestSubj}_header` : undefined}
|
||||
{...rest}
|
||||
/>
|
||||
<Page
|
||||
restrictWidth={restrictWidth || 1200}
|
||||
data-test-subj={dataTestSubj ? `${dataTestSubj}_page` : undefined}
|
||||
>
|
||||
<EuiPageBody>
|
||||
<ContentWrapper>
|
||||
<EuiSpacer size="m" />
|
||||
{children}
|
||||
</ContentWrapper>
|
||||
</EuiPageBody>
|
||||
</Page>
|
||||
</Fragment>
|
||||
);
|
|
@ -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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { Fragment } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { EuiPage, EuiPageBody, EuiSpacer } from '@elastic/eui';
|
||||
|
||||
export const Page = styled(EuiPage)`
|
||||
background: ${(props) => props.theme.eui.euiColorEmptyShade};
|
||||
width: 100%;
|
||||
align-self: center;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
flex: 1;
|
||||
`;
|
||||
|
||||
export const ContentWrapper = styled.div`
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
restrictWidth?: number;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const WithoutHeaderLayout: React.FC<Props> = ({ restrictWidth, children }) => (
|
||||
<Fragment>
|
||||
<Page restrictWidth={restrictWidth || 1200}>
|
||||
<EuiPageBody>
|
||||
<ContentWrapper>
|
||||
<EuiSpacer size="m" />
|
||||
{children}
|
||||
</ContentWrapper>
|
||||
</EuiPageBody>
|
||||
</Page>
|
||||
</Fragment>
|
||||
);
|
|
@ -42,7 +42,8 @@ const OsqueryEditorComponent: React.FC<OsqueryEditorProps> = ({ defaultValue, on
|
|||
name="osquery_editor"
|
||||
setOptions={EDITOR_SET_OPTIONS}
|
||||
editorProps={EDITOR_PROPS}
|
||||
height="200px"
|
||||
height="100px"
|
||||
width="100%"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
/* eslint-disable react/jsx-no-bind */
|
||||
|
||||
/* eslint-disable react-perf/jsx-no-new-function-as-prop */
|
||||
|
||||
import { produce } from 'immer';
|
||||
import { EuiFlyout, EuiTitle, EuiFlyoutBody, EuiFlyoutHeader, EuiPortal } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
|
||||
import { AddPackQueryForm } from '../../packs/common/add_pack_query';
|
||||
|
||||
// @ts-expect-error update types
|
||||
export const AddNewQueryFlyout = ({ data, handleChange, onClose }) => {
|
||||
// @ts-expect-error update types
|
||||
const handleSubmit = (payload) => {
|
||||
// @ts-expect-error update types
|
||||
const updatedPolicy = produce(data, (draft) => {
|
||||
draft.inputs[0].streams.push({
|
||||
data_stream: {
|
||||
type: 'logs',
|
||||
dataset: 'osquery_elastic_managed.osquery',
|
||||
},
|
||||
vars: {
|
||||
query: {
|
||||
type: 'text',
|
||||
value: payload.query.attributes.query,
|
||||
},
|
||||
interval: {
|
||||
type: 'text',
|
||||
value: `${payload.interval}`,
|
||||
},
|
||||
id: {
|
||||
type: 'text',
|
||||
value: payload.query.id,
|
||||
},
|
||||
},
|
||||
enabled: true,
|
||||
});
|
||||
});
|
||||
|
||||
onClose();
|
||||
handleChange({
|
||||
isValid: true,
|
||||
updatedPolicy,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiPortal>
|
||||
<EuiFlyout ownFocus onClose={onClose} aria-labelledby="flyoutTitle">
|
||||
<EuiFlyoutHeader hasBorder>
|
||||
<EuiTitle size="s">
|
||||
<h2 id="flyoutTitle">Attach next query</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
<AddPackQueryForm handleSubmit={handleSubmit} />
|
||||
</EuiFlyoutBody>
|
||||
</EuiFlyout>
|
||||
</EuiPortal>
|
||||
);
|
||||
};
|
|
@ -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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import qs from 'query-string';
|
||||
|
||||
import { Queries } from '../../queries';
|
||||
import { Packs } from '../../packs';
|
||||
import { LiveQuery } from '../../live_query';
|
||||
|
||||
const CustomTabTabsComponent = () => {
|
||||
const location = useLocation();
|
||||
|
||||
const selectedTab = useMemo(() => qs.parse(location.search)?.tab, [location.search]);
|
||||
|
||||
if (selectedTab === 'packs') {
|
||||
return <Packs />;
|
||||
}
|
||||
|
||||
if (selectedTab === 'saved_queries') {
|
||||
return <Queries />;
|
||||
}
|
||||
|
||||
if (selectedTab === 'live_query') {
|
||||
return <LiveQuery />;
|
||||
}
|
||||
|
||||
return <Packs />;
|
||||
};
|
||||
|
||||
export const CustomTabTabs = React.memo(CustomTabTabsComponent);
|
|
@ -0,0 +1,240 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
|
||||
import produce from 'immer';
|
||||
import { find } from 'lodash/fp';
|
||||
import { EuiSpacer, EuiText, EuiHorizontalRule, EuiSuperSelect } from '@elastic/eui';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
import { useQuery } from 'react-query';
|
||||
|
||||
import {
|
||||
// UseField,
|
||||
useForm,
|
||||
useFormData,
|
||||
UseArray,
|
||||
getUseField,
|
||||
Field,
|
||||
ToggleField,
|
||||
Form,
|
||||
} from '../../shared_imports';
|
||||
|
||||
// import { OsqueryStreamField } from '../../scheduled_query/common/osquery_stream_field';
|
||||
import { useKibana } from '../../common/lib/kibana';
|
||||
import { ScheduledQueryQueriesTable } from './scheduled_queries_table';
|
||||
import { schema } from './schema';
|
||||
|
||||
const CommonUseField = getUseField({ component: Field });
|
||||
|
||||
const EDIT_SCHEDULED_QUERY_FORM_ID = 'editScheduledQueryForm';
|
||||
|
||||
interface EditScheduledQueryFormProps {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
data: Array<Record<string, any>>;
|
||||
handleSubmit: () => Promise<void>;
|
||||
}
|
||||
|
||||
const EditScheduledQueryFormComponent: React.FC<EditScheduledQueryFormProps> = ({
|
||||
data,
|
||||
handleSubmit,
|
||||
}) => {
|
||||
const { http } = useKibana().services;
|
||||
|
||||
const {
|
||||
data: { saved_objects: packs } = {
|
||||
saved_objects: [],
|
||||
},
|
||||
} = useQuery('packs', () => http.get('/internal/osquery/pack'));
|
||||
|
||||
const { form } = useForm({
|
||||
id: EDIT_SCHEDULED_QUERY_FORM_ID,
|
||||
onSubmit: handleSubmit,
|
||||
schema,
|
||||
defaultValue: data,
|
||||
options: {
|
||||
stripEmptyFields: false,
|
||||
},
|
||||
// @ts-expect-error update types
|
||||
deserializer: (payload) => {
|
||||
const deserialized = produce(payload, (draft) => {
|
||||
// @ts-expect-error update types
|
||||
draft.streams = draft.inputs[0].streams.map(({ data_stream, enabled, vars }) => ({
|
||||
data: {
|
||||
data_stream,
|
||||
enabled,
|
||||
vars,
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
||||
return deserialized;
|
||||
},
|
||||
// @ts-expect-error update types
|
||||
serializer: (payload) => {
|
||||
const serialized = produce(payload, (draft) => {
|
||||
// @ts-expect-error update types
|
||||
if (draft.inputs) {
|
||||
// @ts-expect-error update types
|
||||
draft.inputs[0].config = {
|
||||
pack: {
|
||||
type: 'id',
|
||||
value: 'e33f5f30-705e-11eb-9e99-9f6b4d0d9506',
|
||||
},
|
||||
};
|
||||
// @ts-expect-error update types
|
||||
draft.inputs[0].type = 'osquery';
|
||||
// @ts-expect-error update types
|
||||
draft.inputs[0].streams = draft.inputs[0].streams?.map((stream) => stream.data) ?? [];
|
||||
}
|
||||
});
|
||||
|
||||
return serialized;
|
||||
},
|
||||
});
|
||||
|
||||
const { setFieldValue } = form;
|
||||
|
||||
const handlePackChange = useCallback(
|
||||
(value) => {
|
||||
const newPack = find(['id', value], packs);
|
||||
|
||||
setFieldValue(
|
||||
'streams',
|
||||
// @ts-expect-error update types
|
||||
newPack.queries.map((packQuery, index) => ({
|
||||
id: index,
|
||||
isNew: true,
|
||||
path: `streams[${index}]`,
|
||||
data: {
|
||||
data_stream: {
|
||||
type: 'logs',
|
||||
dataset: 'osquery_elastic_managed.osquery',
|
||||
},
|
||||
id: 'osquery-osquery_elastic_managed.osquery-7065c2dc-f835-4d13-9486-6eec515f39bd',
|
||||
vars: {
|
||||
query: {
|
||||
type: 'text',
|
||||
value: packQuery.query,
|
||||
},
|
||||
interval: {
|
||||
type: 'text',
|
||||
value: `${packQuery.interval}`,
|
||||
},
|
||||
id: {
|
||||
type: 'text',
|
||||
value: packQuery.id,
|
||||
},
|
||||
},
|
||||
enabled: true,
|
||||
},
|
||||
}))
|
||||
);
|
||||
},
|
||||
[packs, setFieldValue]
|
||||
);
|
||||
|
||||
const [formData] = useFormData({ form, watch: ['streams'] });
|
||||
|
||||
const scheduledQueries = useMemo(() => {
|
||||
if (formData.inputs) {
|
||||
// @ts-expect-error update types
|
||||
return formData.streams.reduce((acc, stream) => {
|
||||
if (!stream.data) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
return [...acc, stream.data];
|
||||
}, []);
|
||||
}
|
||||
|
||||
return [];
|
||||
}, [formData]);
|
||||
|
||||
return (
|
||||
<Form form={form}>
|
||||
<EuiSuperSelect
|
||||
// @ts-expect-error update types
|
||||
options={packs.map((pack) => ({
|
||||
value: pack.id,
|
||||
inputDisplay: (
|
||||
<>
|
||||
<EuiText>{pack.name}</EuiText>
|
||||
<EuiText size="s" color="subdued">
|
||||
<p className="euiTextColor--subdued">{pack.description}</p>
|
||||
</EuiText>
|
||||
</>
|
||||
),
|
||||
}))}
|
||||
valueOfSelected={packs[0]?.id}
|
||||
onChange={handlePackChange}
|
||||
/>
|
||||
<ScheduledQueryQueriesTable data={scheduledQueries} />
|
||||
<CommonUseField path="enabled" component={ToggleField} />
|
||||
<EuiHorizontalRule />
|
||||
<EuiSpacer />
|
||||
<UseArray path="streams" readDefaultValueOnForm={true}>
|
||||
{
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
({ items, form: streamsForm, addItem, removeItem }) => {
|
||||
return (
|
||||
<>
|
||||
{/* {items.map((item) => {
|
||||
return (
|
||||
<UseField
|
||||
key={item.path}
|
||||
path={`${item.path}.data`}
|
||||
component={OsqueryStreamField}
|
||||
// eslint-disable-next-line react/jsx-no-bind, react-perf/jsx-no-new-function-as-prop
|
||||
removeItem={() => removeItem(item.id)}
|
||||
// readDefaultValueOnForm={true}
|
||||
defaultValue={
|
||||
item.isNew
|
||||
? // eslint-disable-next-line react-perf/jsx-no-new-object-as-prop
|
||||
{
|
||||
data_stream: {
|
||||
type: 'logs',
|
||||
dataset: 'osquery_elastic_managed.osquery',
|
||||
},
|
||||
vars: {
|
||||
query: {
|
||||
type: 'text',
|
||||
value: 'select * from uptime',
|
||||
},
|
||||
interval: {
|
||||
type: 'text',
|
||||
value: '120',
|
||||
},
|
||||
id: {
|
||||
type: 'text',
|
||||
value: uuid.v4(),
|
||||
},
|
||||
},
|
||||
enabled: true,
|
||||
}
|
||||
: get(item.path, streamsForm.getFormData())
|
||||
}
|
||||
/>
|
||||
);
|
||||
})} */}
|
||||
{/* <EuiButtonEmpty onClick={addItem} iconType="plusInCircleFilled">
|
||||
{'Add query'}
|
||||
</EuiButtonEmpty> */}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
</UseArray>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export const EditScheduledQueryForm = React.memo(
|
||||
EditScheduledQueryFormComponent,
|
||||
(prevProps, nextProps) => deepEqual(prevProps.data, nextProps.data)
|
||||
);
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { useForm, Form, getUseField, Field, FIELD_TYPES } from '../../shared_imports';
|
||||
|
||||
const CommonUseField = getUseField({ component: Field });
|
||||
|
||||
const FORM_ID = 'inputStreamForm';
|
||||
|
||||
const schema = {
|
||||
data_stream: {
|
||||
dataset: {
|
||||
type: FIELD_TYPES.TEXT,
|
||||
},
|
||||
type: {
|
||||
type: FIELD_TYPES.TEXT,
|
||||
},
|
||||
},
|
||||
enabled: {
|
||||
type: FIELD_TYPES.TOGGLE,
|
||||
label: 'Active',
|
||||
},
|
||||
id: {
|
||||
type: FIELD_TYPES.TEXT,
|
||||
},
|
||||
vars: {
|
||||
id: {
|
||||
type: {
|
||||
type: FIELD_TYPES.TEXT,
|
||||
},
|
||||
value: { type: FIELD_TYPES.TEXT },
|
||||
},
|
||||
interval: {
|
||||
type: {
|
||||
type: FIELD_TYPES.TEXT,
|
||||
},
|
||||
value: { type: FIELD_TYPES.TEXT },
|
||||
},
|
||||
query: {
|
||||
type: {
|
||||
type: FIELD_TYPES.TEXT,
|
||||
},
|
||||
value: { type: FIELD_TYPES.TEXT },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// @ts-expect-error update types
|
||||
const InputStreamFormComponent = ({ data }) => {
|
||||
const { form } = useForm({
|
||||
id: FORM_ID,
|
||||
schema,
|
||||
defaultValue: data,
|
||||
});
|
||||
|
||||
return (
|
||||
<Form form={form}>
|
||||
<CommonUseField path="vars.query.value" />
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export const InputStreamForm = React.memo(InputStreamFormComponent);
|
|
@ -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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
/* eslint-disable react-perf/jsx-no-new-object-as-prop */
|
||||
|
||||
/* eslint-disable react-perf/jsx-no-new-array-as-prop */
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import produce from 'immer';
|
||||
import { EuiRadioGroup } from '@elastic/eui';
|
||||
|
||||
// @ts-expect-error update types
|
||||
export const ScheduledQueryInputType = ({ data, handleChange }) => {
|
||||
const radios = [
|
||||
{
|
||||
id: 'pack',
|
||||
label: 'Pack',
|
||||
},
|
||||
{
|
||||
id: 'saved_queries',
|
||||
label: 'Saved queries',
|
||||
},
|
||||
];
|
||||
|
||||
const onChange = useCallback(
|
||||
(optionId: string) => {
|
||||
// @ts-expect-error update types
|
||||
const updatedPolicy = produce(data, (draft) => {
|
||||
if (!draft.inputs[0].config) {
|
||||
draft.inputs[0].config = {
|
||||
input_source: {
|
||||
type: 'text',
|
||||
value: optionId,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
draft.inputs[0].config.input_source.value = optionId;
|
||||
}
|
||||
});
|
||||
|
||||
handleChange({
|
||||
isValid: true,
|
||||
updatedPolicy,
|
||||
});
|
||||
},
|
||||
[data, handleChange]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiRadioGroup
|
||||
options={radios}
|
||||
idSelected={data.inputs[0].config?.input_source?.value ?? 'saved_queries'}
|
||||
onChange={onChange}
|
||||
name="radio group"
|
||||
legend={{
|
||||
children: <span>{'Choose input type'}</span>,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { snakeCase } from 'lodash/fp';
|
||||
import { EuiIcon, EuiSideNav } from '@elastic/eui';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
import qs from 'query-string';
|
||||
|
||||
export const Navigation = () => {
|
||||
const { push } = useHistory();
|
||||
const location = useLocation();
|
||||
|
||||
const selectedItemName = useMemo(() => qs.parse(location.search)?.tab, [location.search]);
|
||||
|
||||
const handleTabClick = useCallback(
|
||||
(tab) => {
|
||||
push({
|
||||
search: qs.stringify({ tab }),
|
||||
});
|
||||
},
|
||||
[push]
|
||||
);
|
||||
|
||||
const createItem = useCallback(
|
||||
(name, data = {}) => ({
|
||||
...data,
|
||||
id: snakeCase(name),
|
||||
name,
|
||||
isSelected: selectedItemName === name,
|
||||
onClick: () => handleTabClick(snakeCase(name)),
|
||||
}),
|
||||
[handleTabClick, selectedItemName]
|
||||
);
|
||||
|
||||
const sideNav = useMemo(
|
||||
() => [
|
||||
createItem('Packs', {
|
||||
forceOpen: true,
|
||||
items: [
|
||||
createItem('List', {
|
||||
icon: <EuiIcon type="list" />,
|
||||
}),
|
||||
createItem('New pack', {
|
||||
icon: <EuiIcon type="listAdd" />,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
createItem('Saved Queries', {
|
||||
forceOpen: true,
|
||||
items: [
|
||||
createItem('List', {
|
||||
icon: <EuiIcon type="list" />,
|
||||
}),
|
||||
createItem('New query', {
|
||||
icon: <EuiIcon type="listAdd" />,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
// createItem('Scheduled Queries', {
|
||||
// forceOpen: true,
|
||||
// items: [
|
||||
// createItem('List', {
|
||||
// icon: <EuiIcon type="list" />,
|
||||
// }),
|
||||
// createItem('Schedule new query', {
|
||||
// icon: <EuiIcon type="listAdd" />,
|
||||
// }),
|
||||
// ],
|
||||
// }),
|
||||
createItem('Live Query', {
|
||||
forceOpen: true,
|
||||
items: [
|
||||
createItem('Run', {
|
||||
icon: <EuiIcon type="play" />,
|
||||
}),
|
||||
createItem('History', {
|
||||
icon: <EuiIcon type="tableDensityNormal" />,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
[createItem]
|
||||
);
|
||||
|
||||
// eslint-disable-next-line react-perf/jsx-no-new-object-as-prop
|
||||
return <EuiSideNav items={sideNav} style={{ width: 200 }} />;
|
||||
};
|
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
/* eslint-disable react/jsx-no-bind */
|
||||
|
||||
/* eslint-disable react-perf/jsx-no-new-function-as-prop */
|
||||
|
||||
import { find } from 'lodash/fp';
|
||||
import { produce } from 'immer';
|
||||
import { EuiText, EuiSuperSelect } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
|
||||
import { useKibana } from '../../common/lib/kibana';
|
||||
|
||||
// @ts-expect-error update types
|
||||
export const ScheduledQueryPackSelector = ({ data, handleChange }) => {
|
||||
const { http } = useKibana().services;
|
||||
const {
|
||||
data: { saved_objects: packs } = {
|
||||
saved_objects: [],
|
||||
},
|
||||
} = useQuery('packs', () => http.get('/internal/osquery/pack'));
|
||||
|
||||
// @ts-expect-error update types
|
||||
const handlePackChange = (value) => {
|
||||
const newPack = find(['id', value], packs);
|
||||
|
||||
// @ts-expect-error update types
|
||||
const updatedPolicy = produce(data, (draft) => {
|
||||
draft.inputs[0].config.pack = {
|
||||
type: 'text',
|
||||
value: newPack.id,
|
||||
};
|
||||
// @ts-expect-error update types
|
||||
draft.inputs[0].streams = newPack.queries.map((packQuery) => ({
|
||||
data_stream: {
|
||||
type: 'logs',
|
||||
dataset: 'osquery_elastic_managed.osquery',
|
||||
},
|
||||
vars: {
|
||||
query: {
|
||||
type: 'text',
|
||||
value: packQuery.query,
|
||||
},
|
||||
interval: {
|
||||
type: 'text',
|
||||
value: `${packQuery.interval}`,
|
||||
},
|
||||
id: {
|
||||
type: 'text',
|
||||
value: packQuery.id,
|
||||
},
|
||||
},
|
||||
enabled: true,
|
||||
}));
|
||||
});
|
||||
|
||||
handleChange({
|
||||
isValid: true,
|
||||
updatedPolicy,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiSuperSelect
|
||||
// @ts-expect-error update types
|
||||
options={packs.map((pack) => ({
|
||||
value: pack.id,
|
||||
inputDisplay: (
|
||||
<>
|
||||
<EuiText>{pack.name}</EuiText>
|
||||
<EuiText size="s" color="subdued">
|
||||
<p className="euiTextColor--subdued">{pack.description}</p>
|
||||
</EuiText>
|
||||
</>
|
||||
),
|
||||
}))}
|
||||
valueOfSelected={data.inputs[0].config}
|
||||
onChange={handlePackChange}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,142 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
/* eslint-disable react-perf/jsx-no-new-function-as-prop */
|
||||
|
||||
/* eslint-disable react/jsx-no-bind */
|
||||
|
||||
/* eslint-disable react-perf/jsx-no-new-object-as-prop */
|
||||
|
||||
/* eslint-disable react/display-name */
|
||||
|
||||
/* eslint-disable react-perf/jsx-no-new-array-as-prop */
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
EuiBasicTable,
|
||||
EuiButtonIcon,
|
||||
EuiHealth,
|
||||
EuiDescriptionList,
|
||||
RIGHT_ALIGNMENT,
|
||||
} from '@elastic/eui';
|
||||
|
||||
// @ts-expect-error update types
|
||||
export const ScheduledQueryQueriesTable = ({ data }) => {
|
||||
const [pageIndex, setPageIndex] = useState(0);
|
||||
const [pageSize, setPageSize] = useState(5);
|
||||
const [sortField, setSortField] = useState('firstName');
|
||||
const [sortDirection, setSortDirection] = useState('asc');
|
||||
const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState({});
|
||||
|
||||
const onTableChange = ({ page = {}, sort = {} }) => {
|
||||
// @ts-expect-error update types
|
||||
const { index, size } = page;
|
||||
// @ts-expect-error update types
|
||||
const { field, direction } = sort;
|
||||
|
||||
setPageIndex(index);
|
||||
setPageSize(size);
|
||||
setSortField(field);
|
||||
setSortDirection(direction);
|
||||
};
|
||||
|
||||
// @ts-expect-error update types
|
||||
const toggleDetails = (item) => {
|
||||
const itemIdToExpandedRowMapValues = { ...itemIdToExpandedRowMap };
|
||||
// @ts-expect-error update types
|
||||
if (itemIdToExpandedRowMapValues[item.id]) {
|
||||
// @ts-expect-error update types
|
||||
delete itemIdToExpandedRowMapValues[item.id];
|
||||
} else {
|
||||
const { online } = item;
|
||||
const color = online ? 'success' : 'danger';
|
||||
const label = online ? 'Online' : 'Offline';
|
||||
const listItems = [
|
||||
{
|
||||
title: 'Online',
|
||||
description: <EuiHealth color={color}>{label}</EuiHealth>,
|
||||
},
|
||||
];
|
||||
// @ts-expect-error update types
|
||||
itemIdToExpandedRowMapValues[item.id] = <EuiDescriptionList listItems={listItems} />;
|
||||
}
|
||||
setItemIdToExpandedRowMap(itemIdToExpandedRowMapValues);
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
field: 'vars.id.value',
|
||||
name: 'ID',
|
||||
},
|
||||
{
|
||||
field: 'vars.interval.value',
|
||||
name: 'Interval',
|
||||
},
|
||||
{
|
||||
field: 'enabled',
|
||||
name: 'Active',
|
||||
},
|
||||
{
|
||||
name: 'Actions',
|
||||
actions: [
|
||||
{
|
||||
name: 'Clone',
|
||||
description: 'Clone this person',
|
||||
type: 'icon',
|
||||
icon: 'copy',
|
||||
onClick: () => '',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
align: RIGHT_ALIGNMENT,
|
||||
width: '40px',
|
||||
isExpander: true,
|
||||
// @ts-expect-error update types
|
||||
render: (item) => (
|
||||
<EuiButtonIcon
|
||||
onClick={() => toggleDetails(item)}
|
||||
// @ts-expect-error update types
|
||||
aria-label={itemIdToExpandedRowMap[item.id] ? 'Collapse' : 'Expand'}
|
||||
// @ts-expect-error update types
|
||||
iconType={itemIdToExpandedRowMap[item.id] ? 'arrowUp' : 'arrowDown'}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const pagination = {
|
||||
pageIndex,
|
||||
pageSize,
|
||||
totalItemCount: data.inputs[0].streams.length,
|
||||
pageSizeOptions: [3, 5, 8],
|
||||
};
|
||||
|
||||
const sorting = {
|
||||
sort: {
|
||||
field: sortField,
|
||||
direction: sortDirection,
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiBasicTable
|
||||
items={data.inputs[0].streams}
|
||||
itemId="id"
|
||||
itemIdToExpandedRowMap={itemIdToExpandedRowMap}
|
||||
isExpandable={true}
|
||||
hasActions={true}
|
||||
// @ts-expect-error update types
|
||||
columns={columns}
|
||||
pagination={pagination}
|
||||
// @ts-expect-error update types
|
||||
sorting={sorting}
|
||||
isSelectable={true}
|
||||
onChange={onTableChange}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { FIELD_TYPES } from '../../shared_imports';
|
||||
|
||||
export const schema = {
|
||||
name: {
|
||||
type: FIELD_TYPES.TEXT,
|
||||
label: 'Name',
|
||||
},
|
||||
description: {
|
||||
type: FIELD_TYPES.TEXT,
|
||||
label: 'Description',
|
||||
},
|
||||
namespace: {
|
||||
type: FIELD_TYPES.TEXT,
|
||||
},
|
||||
enabled: {
|
||||
type: FIELD_TYPES.TOGGLE,
|
||||
},
|
||||
policy_id: {
|
||||
type: FIELD_TYPES.TEXT,
|
||||
},
|
||||
streams: {
|
||||
type: FIELD_TYPES.MULTI_SELECT,
|
||||
vars: {
|
||||
query: {
|
||||
type: {
|
||||
type: FIELD_TYPES.TEXT,
|
||||
},
|
||||
value: {
|
||||
type: FIELD_TYPES.TEXT,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
12
x-pack/plugins/osquery/public/fleet_integration/index.ts
Normal file
12
x-pack/plugins/osquery/public/fleet_integration/index.ts
Normal 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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export * from './lazy_osquery_managed_empty_create_policy_extension';
|
||||
export * from './lazy_osquery_managed_empty_edit_policy_extension';
|
||||
export * from './lazy_osquery_managed_policy_create_extension';
|
||||
export * from './lazy_osquery_managed_policy_edit_extension';
|
||||
export * from './lazy_osquery_managed_custom_extension';
|
|
@ -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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { lazy } from 'react';
|
||||
import { PackageCustomExtensionComponent } from '../../../fleet/public';
|
||||
|
||||
export const LazyOsqueryManagedCustomExtension = lazy<PackageCustomExtensionComponent>(async () => {
|
||||
const { OsqueryManagedCustomExtension } = await import('./osquery_managed_custom_extension');
|
||||
return {
|
||||
default: OsqueryManagedCustomExtension,
|
||||
};
|
||||
});
|
|
@ -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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { lazy } from 'react';
|
||||
import { PackagePolicyCreateExtensionComponent } from '../../../fleet/public';
|
||||
|
||||
export const LazyOsqueryManagedEmptyCreatePolicyExtension = lazy<PackagePolicyCreateExtensionComponent>(
|
||||
async () => {
|
||||
const { OsqueryManagedEmptyCreatePolicyExtension } = await import(
|
||||
'./osquery_managed_empty_create_policy_extension'
|
||||
);
|
||||
return {
|
||||
default: OsqueryManagedEmptyCreatePolicyExtension,
|
||||
};
|
||||
}
|
||||
);
|
|
@ -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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { lazy } from 'react';
|
||||
import { PackagePolicyEditExtensionComponent } from '../../../fleet/public';
|
||||
|
||||
export const LazyOsqueryManagedEmptyEditPolicyExtension = lazy<PackagePolicyEditExtensionComponent>(
|
||||
async () => {
|
||||
const { OsqueryManagedEmptyEditPolicyExtension } = await import(
|
||||
'./osquery_managed_empty_edit_policy_extension'
|
||||
);
|
||||
return {
|
||||
default: OsqueryManagedEmptyEditPolicyExtension,
|
||||
};
|
||||
}
|
||||
);
|
|
@ -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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { lazy } from 'react';
|
||||
import { PackagePolicyCreateExtensionComponent } from '../../../fleet/public';
|
||||
|
||||
export const LazyOsqueryManagedPolicyCreateExtension = lazy<PackagePolicyCreateExtensionComponent>(
|
||||
async () => {
|
||||
const { OsqueryManagedPolicyCreateExtension } = await import(
|
||||
'./osquery_managed_policy_create_extension'
|
||||
);
|
||||
return {
|
||||
default: OsqueryManagedPolicyCreateExtension,
|
||||
};
|
||||
}
|
||||
);
|
|
@ -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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { lazy } from 'react';
|
||||
import { PackagePolicyEditExtensionComponent } from '../../../fleet/public';
|
||||
|
||||
export const LazyOsqueryManagedPolicyEditExtension = lazy<PackagePolicyEditExtensionComponent>(
|
||||
async () => {
|
||||
const { OsqueryManagedPolicyCreateExtension } = await import(
|
||||
'./osquery_managed_policy_create_extension'
|
||||
);
|
||||
return {
|
||||
default: OsqueryManagedPolicyCreateExtension,
|
||||
};
|
||||
}
|
||||
);
|
|
@ -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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
|
||||
import { PackageCustomExtensionComponentProps } from '../../../fleet/public';
|
||||
import { CustomTabTabs } from './components/custom_tab_tabs';
|
||||
import { Navigation } from './components/navigation';
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
/**
|
||||
* Exports Osquery-specific package policy instructions
|
||||
* for use in the Fleet app custom tab
|
||||
*/
|
||||
export const OsqueryManagedCustomExtension = React.memo<PackageCustomExtensionComponentProps>(
|
||||
() => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false}>
|
||||
<Navigation />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<CustomTabTabs />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</QueryClientProvider>
|
||||
)
|
||||
);
|
||||
OsqueryManagedCustomExtension.displayName = 'OsqueryManagedCustomExtension';
|
|
@ -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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
import { produce } from 'immer';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
|
||||
import { PackagePolicyCreateExtensionComponentProps } from '../../../fleet/public';
|
||||
|
||||
/**
|
||||
* Exports Osquery-specific package policy instructions
|
||||
* for use in the Fleet app create / edit package policy
|
||||
*/
|
||||
const OsqueryManagedEmptyCreatePolicyExtensionComponent: React.FC<PackagePolicyCreateExtensionComponentProps> = ({
|
||||
onChange,
|
||||
newPolicy,
|
||||
}) => {
|
||||
useEffect(() => {
|
||||
const updatedPolicy = produce(newPolicy, (draft) => {
|
||||
draft.inputs.forEach((input) => (input.streams = []));
|
||||
});
|
||||
|
||||
onChange({
|
||||
isValid: true,
|
||||
updatedPolicy,
|
||||
});
|
||||
});
|
||||
|
||||
return <></>;
|
||||
};
|
||||
|
||||
OsqueryManagedEmptyCreatePolicyExtensionComponent.displayName =
|
||||
'OsqueryManagedEmptyCreatePolicyExtension';
|
||||
|
||||
export const OsqueryManagedEmptyCreatePolicyExtension = React.memo(
|
||||
OsqueryManagedEmptyCreatePolicyExtensionComponent,
|
||||
// we don't want to update the component if onChange has changed
|
||||
(prevProps, nextProps) => deepEqual(prevProps.newPolicy, nextProps.newPolicy)
|
||||
);
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { PackagePolicyEditExtensionComponentProps } from '../../../fleet/public';
|
||||
|
||||
/**
|
||||
* Exports Osquery-specific package policy instructions
|
||||
* for use in the Fleet app edit package policy
|
||||
*/
|
||||
const OsqueryManagedEmptyEditPolicyExtensionComponent = () => <></>;
|
||||
|
||||
OsqueryManagedEmptyEditPolicyExtensionComponent.displayName =
|
||||
'OsqueryManagedEmptyEditPolicyExtension';
|
||||
|
||||
export const OsqueryManagedEmptyEditPolicyExtension = React.memo<PackagePolicyEditExtensionComponentProps>(
|
||||
OsqueryManagedEmptyEditPolicyExtensionComponent
|
||||
);
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiButton } from '@elastic/eui';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
|
||||
import { PackagePolicyCreateExtensionComponentProps } from '../../../fleet/public';
|
||||
import { ScheduledQueryInputType } from './components/input_type';
|
||||
import { ScheduledQueryPackSelector } from './components/pack_selector';
|
||||
import { ScheduledQueryQueriesTable } from './components/scheduled_queries_table';
|
||||
import { AddNewQueryFlyout } from './components/add_new_query_flyout';
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
/**
|
||||
* Exports Osquery-specific package policy instructions
|
||||
* for use in the Fleet app create / edit package policy
|
||||
*/
|
||||
export const OsqueryManagedPolicyCreateExtension = React.memo<PackagePolicyCreateExtensionComponentProps>(
|
||||
({ onChange, newPolicy }) => {
|
||||
const [showAddQueryFlyout, setShowAddQueryFlyout] = useState(false);
|
||||
|
||||
const handleShowFlyout = useCallback(() => setShowAddQueryFlyout(true), []);
|
||||
const handleHideFlyout = useCallback(() => setShowAddQueryFlyout(false), []);
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ScheduledQueryInputType data={newPolicy} handleChange={onChange} />
|
||||
{newPolicy.inputs[0].config?.input_source?.value === 'pack' && (
|
||||
<ScheduledQueryPackSelector data={newPolicy} handleChange={onChange} />
|
||||
)}
|
||||
{newPolicy.inputs[0].streams.length && (
|
||||
// @ts-expect-error update types
|
||||
<ScheduledQueryQueriesTable data={newPolicy} handleChange={onChange} />
|
||||
)}
|
||||
{newPolicy.inputs[0].config?.input_source?.value !== 'pack' && (
|
||||
<EuiButton fill onClick={handleShowFlyout}>
|
||||
{'Attach next query'}
|
||||
</EuiButton>
|
||||
)}
|
||||
{showAddQueryFlyout && (
|
||||
<AddNewQueryFlyout data={newPolicy} handleChange={onChange} onClose={handleHideFlyout} />
|
||||
)}
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
);
|
||||
OsqueryManagedPolicyCreateExtension.displayName = 'OsqueryManagedPolicyCreateExtension';
|
|
@ -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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiCodeBlock, EuiSpacer } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { useActionDetails } from '../../actions/use_action_details';
|
||||
import { ResultsTable } from '../../results/results_table';
|
||||
|
||||
const QueryAgentResultsComponent = () => {
|
||||
const { actionId, agentId } = useParams<{ actionId: string; agentId: string }>();
|
||||
const { data } = useActionDetails({ actionId });
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiCodeBlock language="sql" fontSize="m" paddingSize="m">
|
||||
{
|
||||
// @ts-expect-error update types
|
||||
data?.actionDetails._source?.data?.query
|
||||
}
|
||||
</EuiCodeBlock>
|
||||
<EuiSpacer />
|
||||
<ResultsTable actionId={actionId} agentId={agentId} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const QueryAgentResults = React.memo(QueryAgentResultsComponent);
|
|
@ -1,38 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
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);
|
|
@ -6,40 +6,34 @@
|
|||
*/
|
||||
|
||||
import { EuiButton, EuiSpacer } from '@elastic/eui';
|
||||
import React, { useCallback } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import { UseField, Form, useForm } from '../../shared_imports';
|
||||
import { AgentsTableField } from './agents_table_field';
|
||||
import { CodeEditorField } from './code_editor_field';
|
||||
import { LiveQueryQueryField } from './live_query_query_field';
|
||||
|
||||
const FORM_ID = 'liveQueryForm';
|
||||
|
||||
interface LiveQueryFormProps {
|
||||
actionDetails?: Record<string, string>;
|
||||
defaultValue?: unknown;
|
||||
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 LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({ defaultValue, onSubmit }) => {
|
||||
const { form } = useForm({
|
||||
id: FORM_ID,
|
||||
// schema: formSchema,
|
||||
onSubmit: handleSubmit,
|
||||
onSubmit,
|
||||
options: {
|
||||
stripEmptyFields: false,
|
||||
},
|
||||
defaultValue: actionDetails,
|
||||
deserializer: ({ fields, _source }) => ({
|
||||
agents: fields?.agents,
|
||||
command: _source?.data?.commands[0],
|
||||
}),
|
||||
defaultValue: {
|
||||
// @ts-expect-error update types
|
||||
query: defaultValue ?? {
|
||||
id: null,
|
||||
query: '',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { submit } = form;
|
||||
|
@ -48,8 +42,9 @@ const LiveQueryFormComponent: React.FC<LiveQueryFormProps> = ({ actionDetails, o
|
|||
<Form form={form}>
|
||||
<UseField path="agents" component={AgentsTableField} />
|
||||
<EuiSpacer />
|
||||
<UseField path="command" component={CodeEditorField} />
|
||||
<EuiButton onClick={submit}>Send query</EuiButton>
|
||||
<UseField path="query" component={LiveQueryQueryField} />
|
||||
<EuiSpacer />
|
||||
<EuiButton onClick={submit}>{'Send query'}</EuiButton>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
// import { find } from 'lodash/fp';
|
||||
// import { EuiCodeBlock, EuiSuperSelect, EuiText, EuiSpacer } from '@elastic/eui';
|
||||
import React, { useCallback } from 'react';
|
||||
// import { useQuery } from 'react-query';
|
||||
|
||||
import { FieldHook } from '../../shared_imports';
|
||||
// import { useKibana } from '../../common/lib/kibana';
|
||||
import { OsqueryEditor } from '../../editor';
|
||||
|
||||
interface LiveQueryQueryFieldProps {
|
||||
field: FieldHook<{
|
||||
id: string | null;
|
||||
query: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
const LiveQueryQueryFieldComponent: React.FC<LiveQueryQueryFieldProps> = ({ field }) => {
|
||||
// const { http } = useKibana().services;
|
||||
// const { data } = useQuery('savedQueryList', () =>
|
||||
// http.get('/internal/osquery/saved_query', {
|
||||
// query: {
|
||||
// pageIndex: 0,
|
||||
// pageSize: 100,
|
||||
// sortField: 'updated_at',
|
||||
// sortDirection: 'desc',
|
||||
// },
|
||||
// })
|
||||
// );
|
||||
|
||||
// const queryOptions =
|
||||
// // @ts-expect-error update types
|
||||
// data?.saved_objects.map((savedQuery) => ({
|
||||
// value: savedQuery,
|
||||
// inputDisplay: savedQuery.attributes.name,
|
||||
// dropdownDisplay: (
|
||||
// <>
|
||||
// <strong>{savedQuery.attributes.name}</strong>
|
||||
// <EuiText size="s" color="subdued">
|
||||
// <p className="euiTextColor--subdued">{savedQuery.attributes.description}</p>
|
||||
// </EuiText>
|
||||
// <EuiCodeBlock language="sql" fontSize="s" paddingSize="s">
|
||||
// {savedQuery.attributes.query}
|
||||
// </EuiCodeBlock>
|
||||
// </>
|
||||
// ),
|
||||
// })) ?? [];
|
||||
|
||||
const { value, setValue } = field;
|
||||
|
||||
// const handleSavedQueryChange = useCallback(
|
||||
// (newValue) => {
|
||||
// setValue({
|
||||
// id: newValue.id,
|
||||
// query: newValue.attributes.query,
|
||||
// });
|
||||
// },
|
||||
// [setValue]
|
||||
// );
|
||||
|
||||
const handleEditorChange = useCallback(
|
||||
(newValue) => {
|
||||
setValue({
|
||||
id: null,
|
||||
query: newValue,
|
||||
});
|
||||
},
|
||||
[setValue]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* <EuiSuperSelect
|
||||
valueOfSelected={find(['id', value.id], data?.saved_objects)}
|
||||
options={queryOptions}
|
||||
onChange={handleSavedQueryChange}
|
||||
/>
|
||||
<EuiSpacer /> */}
|
||||
<OsqueryEditor defaultValue={value.query} onChange={handleEditorChange} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const LiveQueryQueryField = React.memo(LiveQueryQueryFieldComponent);
|
|
@ -11,7 +11,7 @@ export const formSchema: FormSchema = {
|
|||
agents: {
|
||||
type: FIELD_TYPES.MULTI_SELECT,
|
||||
},
|
||||
command: {
|
||||
query: {
|
||||
type: FIELD_TYPES.TEXTAREA,
|
||||
validations: [],
|
||||
},
|
||||
|
|
|
@ -5,28 +5,42 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiSpacer } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { Switch, Route, useRouteMatch } from 'react-router-dom';
|
||||
import { useMutation } from 'react-query';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import { QueriesPage } from './queries';
|
||||
import { NewLiveQueryPage } from './new';
|
||||
import { EditLiveQueryPage } from './edit';
|
||||
import { useKibana } from '../common/lib/kibana';
|
||||
import { LiveQueryForm } from './form';
|
||||
import { ResultTabs } from '../queries/edit/tabs';
|
||||
|
||||
const LiveQueryComponent = () => {
|
||||
const match = useRouteMatch();
|
||||
const location = useLocation();
|
||||
const { http } = useKibana().services;
|
||||
|
||||
const createActionMutation = useMutation((payload: Record<string, unknown>) =>
|
||||
http.post('/internal/osquery/action', {
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
);
|
||||
|
||||
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>
|
||||
<>
|
||||
{
|
||||
<LiveQueryForm
|
||||
defaultValue={location.state?.query}
|
||||
// @ts-expect-error update types
|
||||
onSubmit={createActionMutation.mutate}
|
||||
/>
|
||||
}
|
||||
|
||||
{createActionMutation.data && (
|
||||
<>
|
||||
<EuiSpacer />
|
||||
<ResultTabs actionId={createActionMutation.data?.action.action_id} />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,30 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
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);
|
|
@ -1,25 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { 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);
|
|
@ -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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
/* eslint-disable react-perf/jsx-no-new-function-as-prop, react/jsx-no-bind */
|
||||
|
||||
import React, { Fragment } from 'react';
|
||||
import { EuiTextArea } from '@elastic/eui';
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { ActionParamsProps } from '../../../triggers_actions_ui/public/types';
|
||||
|
||||
interface ExampleActionParams {
|
||||
message: string;
|
||||
}
|
||||
|
||||
const ExampleParamsFields: React.FunctionComponent<ActionParamsProps<ExampleActionParams>> = ({
|
||||
actionParams,
|
||||
editAction,
|
||||
index,
|
||||
errors,
|
||||
}) => {
|
||||
// console.error('actionParams', actionParams, index, errors);
|
||||
const { message } = actionParams;
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiTextArea
|
||||
fullWidth
|
||||
isInvalid={errors.message.length > 0 && message !== undefined}
|
||||
name="message"
|
||||
value={message || ''}
|
||||
onChange={(e) => {
|
||||
editAction('message', e.target.value, index);
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (!message) {
|
||||
editAction('message', '', index);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
// Export as default in order to support lazy loading
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export { ExampleParamsFields as default };
|
73
x-pack/plugins/osquery/public/osquery_action_type/index.tsx
Normal file
73
x-pack/plugins/osquery/public/osquery_action_type/index.tsx
Normal 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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { lazy } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { ActionTypeModel, ValidationResult } from '../../../triggers_actions_ui/public/types';
|
||||
|
||||
interface ExampleActionParams {
|
||||
message: string;
|
||||
}
|
||||
|
||||
export function getActionType(): ActionTypeModel {
|
||||
return {
|
||||
id: '.osquery',
|
||||
iconClass: 'logoOsquery',
|
||||
selectMessage: i18n.translate(
|
||||
'xpack.osquery.components.builtinActionTypes.exampleAction.selectMessageText',
|
||||
{
|
||||
defaultMessage: 'Example Action is used to show how to create new action type UI.',
|
||||
}
|
||||
),
|
||||
actionTypeTitle: i18n.translate(
|
||||
'xpack.osquery.components.builtinActionTypes.exampleAction.actionTypeTitle',
|
||||
{
|
||||
defaultMessage: 'Example Action',
|
||||
}
|
||||
),
|
||||
// @ts-expect-error update types
|
||||
validateConnector: (action): ValidationResult => {
|
||||
const validationResult = { errors: {} };
|
||||
const errors = {
|
||||
someConnectorField: new Array<string>(),
|
||||
};
|
||||
validationResult.errors = errors;
|
||||
if (!action.config.someConnectorField) {
|
||||
errors.someConnectorField.push(
|
||||
i18n.translate(
|
||||
'xpack.osquery.components.builtinActionTypes.error.requiredSomeConnectorFieldeText',
|
||||
{
|
||||
defaultMessage: 'SomeConnectorField is required.',
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
return validationResult;
|
||||
},
|
||||
validateParams: (actionParams: ExampleActionParams): ValidationResult => {
|
||||
const validationResult = { errors: {} };
|
||||
const errors = {
|
||||
message: new Array<string>(),
|
||||
};
|
||||
validationResult.errors = errors;
|
||||
if (!actionParams.message?.length) {
|
||||
errors.message.push(
|
||||
i18n.translate(
|
||||
'xpack.osquery.components.builtinActionTypes.error.requiredExampleMessageText',
|
||||
{
|
||||
defaultMessage: 'Message is required.',
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
return validationResult;
|
||||
},
|
||||
actionConnectorFields: null,
|
||||
actionParamsFields: lazy(() => import('./example_params_fields')),
|
||||
};
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiFlyout, EuiFlyoutBody, EuiFlyoutHeader, EuiTitle } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
|
||||
import { SavedQueryForm } from '../../queries/form';
|
||||
|
||||
// @ts-expect-error update types
|
||||
const AddNewPackQueryFlyoutComponent = ({ handleClose, handleSubmit }) => (
|
||||
<EuiFlyout ownFocus onClose={handleClose} aria-labelledby="flyoutTitle">
|
||||
<EuiFlyoutHeader hasBorder>
|
||||
<EuiTitle size="m">
|
||||
<h2 id="flyoutTitle">{'Add new Saved Query'}</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
<SavedQueryForm handleSubmit={handleSubmit} />
|
||||
</EuiFlyoutBody>
|
||||
</EuiFlyout>
|
||||
);
|
||||
|
||||
export const AddNewPackQueryFlyout = React.memo(AddNewPackQueryFlyoutComponent);
|
127
x-pack/plugins/osquery/public/packs/common/add_pack_query.tsx
Normal file
127
x-pack/plugins/osquery/public/packs/common/add_pack_query.tsx
Normal file
|
@ -0,0 +1,127 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
/* eslint-disable react-perf/jsx-no-new-object-as-prop */
|
||||
|
||||
import { EuiButton, EuiCodeBlock, EuiSpacer, EuiText, EuiLink, EuiPortal } from '@elastic/eui';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { useQuery, useMutation, useQueryClient } from 'react-query';
|
||||
|
||||
import { getUseField, useForm, Field, Form, FIELD_TYPES } from '../../shared_imports';
|
||||
import { useKibana } from '../../common/lib/kibana';
|
||||
import { AddNewPackQueryFlyout } from './add_new_pack_query_flyout';
|
||||
|
||||
const CommonUseField = getUseField({ component: Field });
|
||||
|
||||
// @ts-expect-error update types
|
||||
const AddPackQueryFormComponent = ({ handleSubmit }) => {
|
||||
const queryClient = useQueryClient();
|
||||
const [showAddQueryFlyout, setShowAddQueryFlyout] = useState(false);
|
||||
|
||||
const { http } = useKibana().services;
|
||||
const { data } = useQuery('savedQueryList', () =>
|
||||
http.get('/internal/osquery/saved_query', {
|
||||
query: {
|
||||
pageIndex: 0,
|
||||
pageSize: 100,
|
||||
sortField: 'updated_at',
|
||||
sortDirection: 'desc',
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const { form } = useForm({
|
||||
id: 'addPackQueryForm',
|
||||
onSubmit: handleSubmit,
|
||||
defaultValue: {
|
||||
query: {},
|
||||
},
|
||||
schema: {
|
||||
query: {
|
||||
type: FIELD_TYPES.SUPER_SELECT,
|
||||
label: 'Pick from Saved Queries',
|
||||
},
|
||||
interval: {
|
||||
type: FIELD_TYPES.NUMBER,
|
||||
label: 'Interval in seconds',
|
||||
},
|
||||
},
|
||||
});
|
||||
const { submit } = form;
|
||||
|
||||
const createSavedQueryMutation = useMutation(
|
||||
(payload) => http.post(`/internal/osquery/saved_query`, { body: JSON.stringify(payload) }),
|
||||
{
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries('savedQueryList');
|
||||
setShowAddQueryFlyout(false);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const queryOptions = useMemo(
|
||||
() =>
|
||||
// @ts-expect-error update types
|
||||
data?.saved_objects.map((savedQuery) => ({
|
||||
value: {
|
||||
id: savedQuery.id,
|
||||
attributes: savedQuery.attributes,
|
||||
type: savedQuery.type,
|
||||
},
|
||||
inputDisplay: savedQuery.attributes.name,
|
||||
dropdownDisplay: (
|
||||
<>
|
||||
<strong>{savedQuery.attributes.name}</strong>
|
||||
<EuiText size="s" color="subdued">
|
||||
<p className="euiTextColor--subdued">{savedQuery.attributes.description}</p>
|
||||
</EuiText>
|
||||
<EuiCodeBlock language="sql" fontSize="m" paddingSize="m">
|
||||
{savedQuery.attributes.query}
|
||||
</EuiCodeBlock>
|
||||
</>
|
||||
),
|
||||
})) ?? [],
|
||||
[data?.saved_objects]
|
||||
);
|
||||
|
||||
const handleShowFlyout = useCallback(() => setShowAddQueryFlyout(true), []);
|
||||
const handleCloseFlyout = useCallback(() => setShowAddQueryFlyout(false), []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form form={form}>
|
||||
<CommonUseField
|
||||
path="query"
|
||||
labelAppend={
|
||||
<EuiText size="xs">
|
||||
<EuiLink onClick={handleShowFlyout}>{'Add new saved query'}</EuiLink>
|
||||
</EuiText>
|
||||
}
|
||||
euiFieldProps={{
|
||||
options: queryOptions,
|
||||
}}
|
||||
/>
|
||||
<EuiSpacer />
|
||||
<CommonUseField path="interval" />
|
||||
<EuiSpacer />
|
||||
<EuiButton fill onClick={submit}>
|
||||
{'Add query'}
|
||||
</EuiButton>
|
||||
</Form>
|
||||
{showAddQueryFlyout && (
|
||||
<EuiPortal>
|
||||
<AddNewPackQueryFlyout
|
||||
handleClose={handleCloseFlyout}
|
||||
handleSubmit={createSavedQueryMutation.mutate}
|
||||
/>
|
||||
</EuiPortal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const AddPackQueryForm = React.memo(AddPackQueryFormComponent);
|
60
x-pack/plugins/osquery/public/packs/common/pack_form.tsx
Normal file
60
x-pack/plugins/osquery/public/packs/common/pack_form.tsx
Normal file
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiButton, EuiSpacer } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
|
||||
import { getUseField, useForm, Field, Form, FIELD_TYPES } from '../../shared_imports';
|
||||
import { PackQueriesField } from './pack_queries_field';
|
||||
|
||||
const CommonUseField = getUseField({ component: Field });
|
||||
|
||||
// @ts-expect-error update types
|
||||
const PackFormComponent = ({ data, handleSubmit }) => {
|
||||
const { form } = useForm({
|
||||
id: 'addPackForm',
|
||||
onSubmit: (payload) => {
|
||||
return handleSubmit(payload);
|
||||
},
|
||||
defaultValue: data ?? {
|
||||
name: '',
|
||||
description: '',
|
||||
queries: [],
|
||||
},
|
||||
schema: {
|
||||
name: {
|
||||
type: FIELD_TYPES.TEXT,
|
||||
label: 'Pack name',
|
||||
},
|
||||
description: {
|
||||
type: FIELD_TYPES.TEXTAREA,
|
||||
label: 'Description',
|
||||
},
|
||||
queries: {
|
||||
type: FIELD_TYPES.MULTI_SELECT,
|
||||
label: 'Queries',
|
||||
},
|
||||
},
|
||||
});
|
||||
const { submit } = form;
|
||||
|
||||
return (
|
||||
<Form form={form}>
|
||||
<CommonUseField path="name" />
|
||||
<EuiSpacer />
|
||||
<CommonUseField path="description" />
|
||||
<EuiSpacer />
|
||||
<CommonUseField path="queries" component={PackQueriesField} />
|
||||
<EuiSpacer />
|
||||
<EuiButton fill onClick={submit}>
|
||||
{'Save pack'}
|
||||
</EuiButton>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export const PackForm = React.memo(PackFormComponent);
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { reject } from 'lodash/fp';
|
||||
import { produce } from 'immer';
|
||||
import { EuiSpacer } from '@elastic/eui';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useQueries } from 'react-query';
|
||||
|
||||
import { useKibana } from '../../common/lib/kibana';
|
||||
import { PackQueriesTable } from '../common/pack_queries_table';
|
||||
import { AddPackQueryForm } from '../common/add_pack_query';
|
||||
|
||||
// @ts-expect-error update types
|
||||
const PackQueriesFieldComponent = ({ field }) => {
|
||||
const { value, setValue } = field;
|
||||
const { http } = useKibana().services;
|
||||
|
||||
const packQueriesData = useQueries(
|
||||
// @ts-expect-error update types
|
||||
value.map((query) => ({
|
||||
queryKey: ['savedQuery', { id: query.id }],
|
||||
queryFn: () => http.get(`/internal/osquery/saved_query/${query.id}`),
|
||||
})) ?? []
|
||||
);
|
||||
|
||||
const packQueries = useMemo(
|
||||
() =>
|
||||
// @ts-expect-error update types
|
||||
packQueriesData.reduce((acc, packQueryData) => {
|
||||
if (packQueryData.data) {
|
||||
return [...acc, packQueryData.data];
|
||||
}
|
||||
return acc;
|
||||
}, []) ?? [],
|
||||
[packQueriesData]
|
||||
);
|
||||
|
||||
const handleAddQuery = useCallback(
|
||||
(newQuery) =>
|
||||
setValue(
|
||||
produce((draft) => {
|
||||
draft.push({
|
||||
interval: newQuery.interval,
|
||||
query: newQuery.query.attributes.query,
|
||||
id: newQuery.query.id,
|
||||
name: newQuery.query.attributes.name,
|
||||
});
|
||||
})
|
||||
),
|
||||
[setValue]
|
||||
);
|
||||
|
||||
const handleRemoveQuery = useCallback(
|
||||
(query) => setValue(produce((draft) => reject(['id', query.id], draft))),
|
||||
[setValue]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PackQueriesTable
|
||||
items={packQueries}
|
||||
config={field.value}
|
||||
handleRemoveQuery={handleRemoveQuery}
|
||||
/>
|
||||
<EuiSpacer />
|
||||
<AddPackQueryForm handleSubmit={handleAddQuery} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const PackQueriesField = React.memo(PackQueriesFieldComponent);
|
|
@ -0,0 +1,140 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-shadow, react-perf/jsx-no-new-object-as-prop, react/jsx-no-bind, react/display-name, react-perf/jsx-no-new-function-as-prop, react-perf/jsx-no-new-array-as-prop */
|
||||
|
||||
import { find } from 'lodash/fp';
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
EuiBasicTable,
|
||||
EuiButtonIcon,
|
||||
EuiHealth,
|
||||
EuiDescriptionList,
|
||||
RIGHT_ALIGNMENT,
|
||||
} from '@elastic/eui';
|
||||
|
||||
// @ts-expect-error update types
|
||||
const PackQueriesTableComponent = ({ items, config, handleRemoveQuery }) => {
|
||||
const [pageIndex, setPageIndex] = useState(0);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const [sortField, setSortField] = useState('firstName');
|
||||
const [sortDirection, setSortDirection] = useState('asc');
|
||||
const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState({});
|
||||
const totalItemCount = 100;
|
||||
|
||||
const onTableChange = ({ page = {}, sort = {} }) => {
|
||||
// @ts-expect-error update types
|
||||
const { index: pageIndex, size: pageSize } = page;
|
||||
|
||||
// @ts-expect-error update types
|
||||
const { field: sortField, direction: sortDirection } = sort;
|
||||
|
||||
setPageIndex(pageIndex);
|
||||
setPageSize(pageSize);
|
||||
setSortField(sortField);
|
||||
setSortDirection(sortDirection);
|
||||
};
|
||||
|
||||
// @ts-expect-error update types
|
||||
const toggleDetails = (item) => {
|
||||
const itemIdToExpandedRowMapValues = { ...itemIdToExpandedRowMap };
|
||||
// @ts-expect-error update types
|
||||
if (itemIdToExpandedRowMapValues[item.id]) {
|
||||
// @ts-expect-error update types
|
||||
delete itemIdToExpandedRowMapValues[item.id];
|
||||
} else {
|
||||
const { online } = item;
|
||||
const color = online ? 'success' : 'danger';
|
||||
const label = online ? 'Online' : 'Offline';
|
||||
const listItems = [
|
||||
{
|
||||
title: 'Nationality',
|
||||
description: `aa`,
|
||||
},
|
||||
{
|
||||
title: 'Online',
|
||||
description: <EuiHealth color={color}>{label}</EuiHealth>,
|
||||
},
|
||||
];
|
||||
// @ts-expect-error update types
|
||||
itemIdToExpandedRowMapValues[item.id] = <EuiDescriptionList listItems={listItems} />;
|
||||
}
|
||||
setItemIdToExpandedRowMap(itemIdToExpandedRowMapValues);
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
field: 'name',
|
||||
name: 'Query Name',
|
||||
},
|
||||
{
|
||||
name: 'Interval',
|
||||
// @ts-expect-error update types
|
||||
render: (query) => find(['name', query.name], config).interval,
|
||||
},
|
||||
{
|
||||
name: 'Actions',
|
||||
actions: [
|
||||
{
|
||||
name: 'Remove',
|
||||
description: 'Remove this query',
|
||||
type: 'icon',
|
||||
icon: 'trash',
|
||||
onClick: handleRemoveQuery,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
align: RIGHT_ALIGNMENT,
|
||||
width: '40px',
|
||||
isExpander: true,
|
||||
// @ts-expect-error update types
|
||||
render: (item) => (
|
||||
<EuiButtonIcon
|
||||
onClick={() => toggleDetails(item)}
|
||||
// @ts-expect-error update types
|
||||
aria-label={itemIdToExpandedRowMap[item.id] ? 'Collapse' : 'Expand'}
|
||||
// @ts-expect-error update types
|
||||
iconType={itemIdToExpandedRowMap[item.id] ? 'arrowUp' : 'arrowDown'}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const pagination = {
|
||||
pageIndex,
|
||||
pageSize,
|
||||
totalItemCount,
|
||||
pageSizeOptions: [3, 5, 8],
|
||||
};
|
||||
|
||||
const sorting = {
|
||||
sort: {
|
||||
field: sortField,
|
||||
direction: sortDirection,
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiBasicTable
|
||||
items={items}
|
||||
itemId="id"
|
||||
itemIdToExpandedRowMap={itemIdToExpandedRowMap}
|
||||
isExpandable={true}
|
||||
hasActions={true}
|
||||
// @ts-expect-error update types
|
||||
columns={columns}
|
||||
pagination={pagination}
|
||||
// @ts-expect-error update types
|
||||
sorting={sorting}
|
||||
isSelectable={true}
|
||||
onChange={onTableChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const PackQueriesTable = React.memo(PackQueriesTableComponent);
|
53
x-pack/plugins/osquery/public/packs/edit/index.tsx
Normal file
53
x-pack/plugins/osquery/public/packs/edit/index.tsx
Normal file
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
/* eslint-disable react-perf/jsx-no-new-object-as-prop */
|
||||
|
||||
import React from 'react';
|
||||
import { useMutation, useQuery } from 'react-query';
|
||||
|
||||
import { PackForm } from '../common/pack_form';
|
||||
import { useKibana } from '../../common/lib/kibana';
|
||||
|
||||
interface EditPackPageProps {
|
||||
onSuccess: () => void;
|
||||
packId: string;
|
||||
}
|
||||
|
||||
const EditPackPageComponent: React.FC<EditPackPageProps> = ({ onSuccess, packId }) => {
|
||||
const { http } = useKibana().services;
|
||||
|
||||
const {
|
||||
data = {
|
||||
queries: [],
|
||||
},
|
||||
} = useQuery(['pack', { id: packId }], ({ queryKey }) => {
|
||||
return http.get(`/internal/osquery/pack/${queryKey[1].id}`);
|
||||
});
|
||||
|
||||
const updatePackMutation = useMutation(
|
||||
(payload) =>
|
||||
http.put(`/internal/osquery/pack/${packId}`, {
|
||||
body: JSON.stringify({
|
||||
...data,
|
||||
// @ts-expect-error update types
|
||||
...payload,
|
||||
}),
|
||||
}),
|
||||
{
|
||||
onSuccess,
|
||||
}
|
||||
);
|
||||
|
||||
if (!data.id) {
|
||||
return <>{'Loading...'}</>;
|
||||
}
|
||||
|
||||
return <PackForm data={data} handleSubmit={updatePackMutation.mutate} />;
|
||||
};
|
||||
|
||||
export const EditPackPage = React.memo(EditPackPageComponent);
|
36
x-pack/plugins/osquery/public/packs/index.tsx
Normal file
36
x-pack/plugins/osquery/public/packs/index.tsx
Normal 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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useState } from 'react';
|
||||
|
||||
import { PacksPage } from './list';
|
||||
import { NewPackPage } from './new';
|
||||
import { EditPackPage } from './edit';
|
||||
|
||||
const PacksComponent = () => {
|
||||
const [showNewPackForm, setShowNewPackForm] = useState(false);
|
||||
const [editPackId, setEditPackId] = useState<string | null>(null);
|
||||
|
||||
const goBack = useCallback(() => {
|
||||
setShowNewPackForm(false);
|
||||
setEditPackId(null);
|
||||
}, []);
|
||||
|
||||
const handleNewQueryClick = useCallback(() => setShowNewPackForm(true), []);
|
||||
|
||||
if (showNewPackForm) {
|
||||
return <NewPackPage onSuccess={goBack} />;
|
||||
}
|
||||
|
||||
if (editPackId?.length) {
|
||||
return <EditPackPage onSuccess={goBack} packId={editPackId} />;
|
||||
}
|
||||
|
||||
return <PacksPage onNewClick={handleNewQueryClick} onEditClick={setEditPackId} />;
|
||||
};
|
||||
|
||||
export const Packs = React.memo(PacksComponent);
|
226
x-pack/plugins/osquery/public/packs/list/index.tsx
Normal file
226
x-pack/plugins/osquery/public/packs/list/index.tsx
Normal file
|
@ -0,0 +1,226 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { map } from 'lodash/fp';
|
||||
import {
|
||||
EuiBasicTable,
|
||||
EuiButton,
|
||||
EuiButtonIcon,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiSpacer,
|
||||
RIGHT_ALIGNMENT,
|
||||
} from '@elastic/eui';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { useQuery, useQueryClient, useMutation } from 'react-query';
|
||||
|
||||
import { PackTableQueriesTable } from './pack_table_queries_table';
|
||||
import { useKibana } from '../../common/lib/kibana';
|
||||
|
||||
interface PacksPageProps {
|
||||
onEditClick: (packId: string) => void;
|
||||
onNewClick: () => void;
|
||||
}
|
||||
|
||||
const PacksPageComponent: React.FC<PacksPageProps> = ({ onNewClick, onEditClick }) => {
|
||||
const queryClient = useQueryClient();
|
||||
const [pageIndex, setPageIndex] = useState(0);
|
||||
const [pageSize, setPageSize] = useState(5);
|
||||
const [sortField, setSortField] = useState('updated_at');
|
||||
const [sortDirection, setSortDirection] = useState('desc');
|
||||
const [selectedItems, setSelectedItems] = useState([]);
|
||||
const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState<Record<string, unknown>>({});
|
||||
const { http } = useKibana().services;
|
||||
|
||||
const deletePacksMutation = useMutation(
|
||||
(payload) => http.delete(`/internal/osquery/pack`, { body: JSON.stringify(payload) }),
|
||||
{
|
||||
onSuccess: () => queryClient.invalidateQueries('packList'),
|
||||
}
|
||||
);
|
||||
|
||||
const { data = {} } = useQuery(
|
||||
['packList', { pageIndex, pageSize, sortField, sortDirection }],
|
||||
() =>
|
||||
http.get('/internal/osquery/pack', {
|
||||
query: {
|
||||
pageIndex,
|
||||
pageSize,
|
||||
sortField,
|
||||
sortDirection,
|
||||
},
|
||||
}),
|
||||
{
|
||||
keepPreviousData: true,
|
||||
// Refetch the data every 10 seconds
|
||||
refetchInterval: 5000,
|
||||
}
|
||||
);
|
||||
const { total = 0, saved_objects: packs } = data;
|
||||
|
||||
const toggleDetails = useCallback(
|
||||
(item) => () => {
|
||||
const itemIdToExpandedRowMapValues = { ...itemIdToExpandedRowMap };
|
||||
if (itemIdToExpandedRowMapValues[item.id]) {
|
||||
delete itemIdToExpandedRowMapValues[item.id];
|
||||
} else {
|
||||
itemIdToExpandedRowMapValues[item.id] = (
|
||||
<>
|
||||
<PackTableQueriesTable items={item.queries} />
|
||||
<EuiSpacer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
setItemIdToExpandedRowMap(itemIdToExpandedRowMapValues);
|
||||
},
|
||||
[itemIdToExpandedRowMap]
|
||||
);
|
||||
|
||||
const renderExtendedItemToggle = useCallback(
|
||||
(item) => (
|
||||
<EuiButtonIcon
|
||||
onClick={toggleDetails(item)}
|
||||
aria-label={itemIdToExpandedRowMap[item.id] ? 'Collapse' : 'Expand'}
|
||||
iconType={itemIdToExpandedRowMap[item.id] ? 'arrowUp' : 'arrowDown'}
|
||||
/>
|
||||
),
|
||||
[itemIdToExpandedRowMap, toggleDetails]
|
||||
);
|
||||
|
||||
const handleEditClick = useCallback((item) => onEditClick(item.id), [onEditClick]);
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{
|
||||
field: 'name',
|
||||
name: 'Pack name',
|
||||
sortable: true,
|
||||
truncateText: true,
|
||||
},
|
||||
{
|
||||
field: 'description',
|
||||
name: 'Description',
|
||||
sortable: true,
|
||||
truncateText: true,
|
||||
},
|
||||
{
|
||||
field: 'queries',
|
||||
name: 'Queries',
|
||||
sortable: false,
|
||||
// @ts-expect-error update types
|
||||
render: (queries) => queries.length,
|
||||
},
|
||||
{
|
||||
field: 'updated_at',
|
||||
name: 'Last updated at',
|
||||
sortable: true,
|
||||
truncateText: true,
|
||||
},
|
||||
{
|
||||
name: 'Actions',
|
||||
actions: [
|
||||
{
|
||||
name: 'Edit',
|
||||
description: 'Edit or run this query',
|
||||
type: 'icon',
|
||||
icon: 'documentEdit',
|
||||
onClick: handleEditClick,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
align: RIGHT_ALIGNMENT,
|
||||
width: '40px',
|
||||
isExpander: true,
|
||||
render: renderExtendedItemToggle,
|
||||
},
|
||||
],
|
||||
[handleEditClick, renderExtendedItemToggle]
|
||||
);
|
||||
|
||||
const onTableChange = useCallback(({ page = {}, sort = {} }) => {
|
||||
setPageIndex(page.index);
|
||||
setPageSize(page.size);
|
||||
setSortField(sort.field);
|
||||
setSortDirection(sort.direction);
|
||||
}, []);
|
||||
|
||||
const pagination = useMemo(
|
||||
() => ({
|
||||
pageIndex,
|
||||
pageSize,
|
||||
totalItemCount: total,
|
||||
pageSizeOptions: [3, 5, 8],
|
||||
}),
|
||||
[total, pageIndex, pageSize]
|
||||
);
|
||||
|
||||
const sorting = useMemo(
|
||||
() => ({
|
||||
sort: {
|
||||
field: sortField,
|
||||
direction: sortDirection,
|
||||
},
|
||||
}),
|
||||
[sortDirection, sortField]
|
||||
);
|
||||
|
||||
const selection = useMemo(
|
||||
() => ({
|
||||
selectable: () => true,
|
||||
onSelectionChange: setSelectedItems,
|
||||
initialSelected: [],
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
const handleDeleteClick = useCallback(() => {
|
||||
const selectedItemsIds = map<string>('id', selectedItems);
|
||||
// @ts-expect-error update types
|
||||
deletePacksMutation.mutate({ packIds: selectedItemsIds });
|
||||
}, [deletePacksMutation, selectedItems]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<EuiFlexGroup justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
{!selectedItems.length ? (
|
||||
<EuiButton fill onClick={onNewClick}>
|
||||
{'New pack'}
|
||||
</EuiButton>
|
||||
) : (
|
||||
<EuiButton color="danger" iconType="trash" onClick={handleDeleteClick}>
|
||||
{`Delete ${selectedItems.length} packs`}
|
||||
</EuiButton>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiSpacer />
|
||||
|
||||
{packs && (
|
||||
<EuiBasicTable
|
||||
items={packs}
|
||||
itemId="id"
|
||||
// @ts-expect-error update types
|
||||
columns={columns}
|
||||
pagination={pagination}
|
||||
// @ts-expect-error update types
|
||||
sorting={sorting}
|
||||
isSelectable={true}
|
||||
selection={selection}
|
||||
onChange={onTableChange}
|
||||
// @ts-expect-error update types
|
||||
itemIdToExpandedRowMap={itemIdToExpandedRowMap}
|
||||
rowHeader="id"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const PacksPage = React.memo(PacksPageComponent);
|
|
@ -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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiBasicTable, EuiCodeBlock } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
|
||||
const columns = [
|
||||
{
|
||||
field: 'id',
|
||||
name: 'ID',
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
name: 'Query name',
|
||||
},
|
||||
{
|
||||
field: 'interval',
|
||||
name: 'Query interval',
|
||||
},
|
||||
{
|
||||
field: 'query',
|
||||
name: 'Query',
|
||||
// eslint-disable-next-line react/display-name
|
||||
render: (query: string) => (
|
||||
<EuiCodeBlock language="sql" fontSize="s" paddingSize="s">
|
||||
{query}
|
||||
</EuiCodeBlock>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
// @ts-expect-error update types
|
||||
const PackTableQueriesTableComponent = ({ items }) => {
|
||||
return <EuiBasicTable compressed items={items} rowHeader="id" columns={columns} />;
|
||||
};
|
||||
|
||||
export const PackTableQueriesTable = React.memo(PackTableQueriesTableComponent);
|
35
x-pack/plugins/osquery/public/packs/new/index.tsx
Normal file
35
x-pack/plugins/osquery/public/packs/new/index.tsx
Normal file
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { useMutation } from 'react-query';
|
||||
|
||||
import { PackForm } from '../common/pack_form';
|
||||
import { useKibana } from '../../common/lib/kibana';
|
||||
|
||||
interface NewPackPageProps {
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
const NewPackPageComponent: React.FC<NewPackPageProps> = ({ onSuccess }) => {
|
||||
const { http } = useKibana().services;
|
||||
|
||||
const addPackMutation = useMutation(
|
||||
(payload) =>
|
||||
http.post(`/internal/osquery/pack`, {
|
||||
body: JSON.stringify(payload),
|
||||
}),
|
||||
{
|
||||
onSuccess,
|
||||
}
|
||||
);
|
||||
|
||||
// @ts-expect-error update types
|
||||
return <PackForm handleSubmit={addPackMutation.mutate} />;
|
||||
};
|
||||
|
||||
export const NewPackPage = React.memo(NewPackPageComponent);
|
|
@ -5,18 +5,47 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { BehaviorSubject, Subject } from 'rxjs';
|
||||
import {
|
||||
AppMountParameters,
|
||||
CoreSetup,
|
||||
Plugin,
|
||||
PluginInitializerContext,
|
||||
CoreStart,
|
||||
} from 'src/core/public';
|
||||
DEFAULT_APP_CATEGORIES,
|
||||
AppStatus,
|
||||
AppUpdater,
|
||||
} from '../../../../src/core/public';
|
||||
import { Storage } from '../../../../src/plugins/kibana_utils/public';
|
||||
import { OsqueryPluginSetup, OsqueryPluginStart, AppPluginStartDependencies } from './types';
|
||||
import {
|
||||
OsqueryPluginSetup,
|
||||
OsqueryPluginStart,
|
||||
// SetupPlugins,
|
||||
StartPlugins,
|
||||
AppPluginStartDependencies,
|
||||
} from './types';
|
||||
import { PLUGIN_NAME } from '../common';
|
||||
import {
|
||||
LazyOsqueryManagedEmptyCreatePolicyExtension,
|
||||
LazyOsqueryManagedEmptyEditPolicyExtension,
|
||||
} from './fleet_integration';
|
||||
// import { getActionType } from './osquery_action_type';
|
||||
|
||||
export function toggleOsqueryPlugin(updater$: Subject<AppUpdater>, http: CoreStart['http']) {
|
||||
http.fetch('/api/fleet/epm/packages', { query: { experimental: true } }).then(({ response }) => {
|
||||
const installed = response.find(
|
||||
// @ts-expect-error update types
|
||||
(integration) =>
|
||||
integration?.name === 'osquery_elastic_managed' && integration?.status === 'installed'
|
||||
);
|
||||
updater$.next(() => ({
|
||||
status: installed ? AppStatus.accessible : AppStatus.inaccessible,
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
export class OsqueryPlugin implements Plugin<OsqueryPluginSetup, OsqueryPluginStart> {
|
||||
private readonly appUpdater$ = new BehaviorSubject<AppUpdater>(() => ({}));
|
||||
private kibanaVersion: string;
|
||||
private storage = new Storage(localStorage);
|
||||
|
||||
|
@ -24,7 +53,10 @@ export class OsqueryPlugin implements Plugin<OsqueryPluginSetup, OsqueryPluginSt
|
|||
this.kibanaVersion = this.initializerContext.env.packageInfo.version;
|
||||
}
|
||||
|
||||
public setup(core: CoreSetup): OsqueryPluginSetup {
|
||||
public setup(
|
||||
core: CoreSetup
|
||||
// plugins: SetupPlugins
|
||||
): OsqueryPluginSetup {
|
||||
const config = this.initializerContext.config.get<{ enabled: boolean }>();
|
||||
|
||||
if (!config.enabled) {
|
||||
|
@ -37,6 +69,9 @@ export class OsqueryPlugin implements Plugin<OsqueryPluginSetup, OsqueryPluginSt
|
|||
core.application.register({
|
||||
id: 'osquery',
|
||||
title: PLUGIN_NAME,
|
||||
order: 9030,
|
||||
updater$: this.appUpdater$,
|
||||
category: DEFAULT_APP_CATEGORIES.management,
|
||||
async mount(params: AppMountParameters) {
|
||||
// Get start services as specified in kibana.json
|
||||
const [coreStart, depsStart] = await core.getStartServices();
|
||||
|
@ -53,13 +88,50 @@ export class OsqueryPlugin implements Plugin<OsqueryPluginSetup, OsqueryPluginSt
|
|||
},
|
||||
});
|
||||
|
||||
// plugins.triggersActionsUi.actionTypeRegistry.register(getActionType());
|
||||
|
||||
// Return methods that should be available to other plugins
|
||||
return {};
|
||||
}
|
||||
|
||||
public start(core: CoreStart): OsqueryPluginStart {
|
||||
public start(core: CoreStart, plugins: StartPlugins): OsqueryPluginStart {
|
||||
const config = this.initializerContext.config.get<{ enabled: boolean }>();
|
||||
|
||||
if (!config.enabled) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (plugins.fleet) {
|
||||
const { registerExtension } = plugins.fleet;
|
||||
|
||||
toggleOsqueryPlugin(this.appUpdater$, core.http);
|
||||
|
||||
registerExtension({
|
||||
package: 'osquery_elastic_managed',
|
||||
view: 'package-policy-create',
|
||||
component: LazyOsqueryManagedEmptyCreatePolicyExtension,
|
||||
});
|
||||
|
||||
registerExtension({
|
||||
package: 'osquery_elastic_managed',
|
||||
view: 'package-policy-edit',
|
||||
component: LazyOsqueryManagedEmptyEditPolicyExtension,
|
||||
});
|
||||
|
||||
// registerExtension({
|
||||
// package: 'osquery_elastic_managed',
|
||||
// view: 'package-detail-custom',
|
||||
// component: LazyOsqueryManagedCustomExtension,
|
||||
// });
|
||||
} else {
|
||||
this.appUpdater$.next(() => ({
|
||||
status: AppStatus.inaccessible,
|
||||
}));
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
public stop() {}
|
||||
}
|
||||
|
|
53
x-pack/plugins/osquery/public/queries/edit/index.tsx
Normal file
53
x-pack/plugins/osquery/public/queries/edit/index.tsx
Normal file
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { isEmpty } from 'lodash/fp';
|
||||
import React from 'react';
|
||||
import { useMutation, useQuery } from 'react-query';
|
||||
|
||||
import { SavedQueryForm } from '../form';
|
||||
import { useKibana } from '../../common/lib/kibana';
|
||||
|
||||
interface EditSavedQueryPageProps {
|
||||
onSuccess: () => void;
|
||||
savedQueryId: string;
|
||||
}
|
||||
|
||||
const EditSavedQueryPageComponent: React.FC<EditSavedQueryPageProps> = ({
|
||||
onSuccess,
|
||||
savedQueryId,
|
||||
}) => {
|
||||
const { http } = useKibana().services;
|
||||
|
||||
const { isLoading, data: savedQueryDetails } = useQuery(['savedQuery', { savedQueryId }], () =>
|
||||
http.get(`/internal/osquery/saved_query/${savedQueryId}`)
|
||||
);
|
||||
const updateSavedQueryMutation = useMutation(
|
||||
(payload) =>
|
||||
http.put(`/internal/osquery/saved_query/${savedQueryId}`, { body: JSON.stringify(payload) }),
|
||||
{ onSuccess }
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return <>{'Loading...'}</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isEmpty(savedQueryDetails) && (
|
||||
<SavedQueryForm
|
||||
defaultValue={savedQueryDetails}
|
||||
// @ts-expect-error update types
|
||||
handleSubmit={updateSavedQueryMutation.mutate}
|
||||
type="edit"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const EditSavedQueryPage = React.memo(EditSavedQueryPageComponent);
|
|
@ -6,14 +6,16 @@
|
|||
*/
|
||||
|
||||
import { EuiTabbedContent, EuiSpacer } from '@elastic/eui';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { ResultsTable } from '../../results/results_table';
|
||||
import { ActionResultsTable } from '../../action_results/action_results_table';
|
||||
|
||||
const ResultTabsComponent = () => {
|
||||
const { actionId } = useParams<{ actionId: string }>();
|
||||
interface ResultTabsProps {
|
||||
actionId: string;
|
||||
}
|
||||
|
||||
const ResultTabsComponent: React.FC<ResultTabsProps> = ({ actionId }) => {
|
||||
const tabs = useMemo(
|
||||
() => [
|
||||
{
|
||||
|
@ -40,17 +42,12 @@ const ResultTabsComponent = () => {
|
|||
[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}
|
||||
expand={false}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -5,28 +5,19 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import { OsqueryEditor } from '../../editor';
|
||||
import { FieldHook } from '../../shared_imports';
|
||||
|
||||
interface CodeEditorFieldProps {
|
||||
field: FieldHook<{ query: string }>;
|
||||
field: FieldHook<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} />;
|
||||
return <OsqueryEditor defaultValue={value} onChange={setValue} />;
|
||||
};
|
||||
|
||||
export const CodeEditorField = React.memo(CodeEditorFieldComponent);
|
72
x-pack/plugins/osquery/public/queries/form/index.tsx
Normal file
72
x-pack/plugins/osquery/public/queries/form/index.tsx
Normal file
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiButton, EuiSpacer } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
|
||||
import { Field, getUseField, useForm, UseField, Form } from '../../shared_imports';
|
||||
import { CodeEditorField } from './code_editor_field';
|
||||
import { formSchema } from './schema';
|
||||
|
||||
export const CommonUseField = getUseField({ component: Field });
|
||||
|
||||
const SAVED_QUERY_FORM_ID = 'savedQueryForm';
|
||||
|
||||
interface SavedQueryFormProps {
|
||||
defaultValue?: unknown;
|
||||
handleSubmit: () => Promise<void>;
|
||||
type?: string;
|
||||
}
|
||||
|
||||
const SavedQueryFormComponent: React.FC<SavedQueryFormProps> = ({
|
||||
defaultValue,
|
||||
handleSubmit,
|
||||
type,
|
||||
}) => {
|
||||
const { form } = useForm({
|
||||
// @ts-expect-error update types
|
||||
id: defaultValue ? SAVED_QUERY_FORM_ID + defaultValue.id : SAVED_QUERY_FORM_ID,
|
||||
schema: formSchema,
|
||||
onSubmit: handleSubmit,
|
||||
options: {
|
||||
stripEmptyFields: false,
|
||||
},
|
||||
// @ts-expect-error update types
|
||||
defaultValue,
|
||||
});
|
||||
|
||||
const { submit } = form;
|
||||
|
||||
return (
|
||||
<Form form={form}>
|
||||
<CommonUseField path="name" />
|
||||
<EuiSpacer />
|
||||
<CommonUseField path="description" />
|
||||
<EuiSpacer />
|
||||
<CommonUseField
|
||||
path="platform"
|
||||
// eslint-disable-next-line react-perf/jsx-no-new-object-as-prop
|
||||
euiFieldProps={{
|
||||
options: [
|
||||
{ value: 'darwin', text: 'macOS' },
|
||||
{ value: 'freebsd', text: 'FreeBSD' },
|
||||
{ value: 'linux', text: 'Linux' },
|
||||
{ value: 'posix', text: 'Posix' },
|
||||
{ value: 'windows', text: 'Windows' },
|
||||
{ value: 'all', text: 'All' },
|
||||
],
|
||||
}}
|
||||
/>
|
||||
<EuiSpacer />
|
||||
<UseField path="query" component={CodeEditorField} />
|
||||
<EuiSpacer />
|
||||
<EuiButton onClick={submit}>{type === 'edit' ? 'Update' : 'Save'}</EuiButton>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export const SavedQueryForm = React.memo(SavedQueryFormComponent);
|
30
x-pack/plugins/osquery/public/queries/form/schema.ts
Normal file
30
x-pack/plugins/osquery/public/queries/form/schema.ts
Normal 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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { FIELD_TYPES, FormSchema } from '../../shared_imports';
|
||||
|
||||
export const formSchema: FormSchema = {
|
||||
name: {
|
||||
type: FIELD_TYPES.TEXT,
|
||||
label: 'Query name',
|
||||
},
|
||||
description: {
|
||||
type: FIELD_TYPES.TEXTAREA,
|
||||
label: 'Description',
|
||||
validations: [],
|
||||
},
|
||||
platform: {
|
||||
type: FIELD_TYPES.SELECT,
|
||||
label: 'Platform',
|
||||
defaultValue: 'all',
|
||||
},
|
||||
query: {
|
||||
label: 'Query',
|
||||
type: FIELD_TYPES.TEXTAREA,
|
||||
validations: [],
|
||||
},
|
||||
};
|
36
x-pack/plugins/osquery/public/queries/index.tsx
Normal file
36
x-pack/plugins/osquery/public/queries/index.tsx
Normal 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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useState } from 'react';
|
||||
|
||||
import { QueriesPage } from './queries';
|
||||
import { NewSavedQueryPage } from './new';
|
||||
import { EditSavedQueryPage } from './edit';
|
||||
|
||||
const QueriesComponent = () => {
|
||||
const [showNewSavedQueryForm, setShowNewSavedQueryForm] = useState(false);
|
||||
const [editSavedQueryId, setEditSavedQueryId] = useState<string | null>(null);
|
||||
|
||||
const goBack = useCallback(() => {
|
||||
setShowNewSavedQueryForm(false);
|
||||
setEditSavedQueryId(null);
|
||||
}, []);
|
||||
|
||||
const handleNewQueryClick = useCallback(() => setShowNewSavedQueryForm(true), []);
|
||||
|
||||
if (showNewSavedQueryForm) {
|
||||
return <NewSavedQueryPage onSuccess={goBack} />;
|
||||
}
|
||||
|
||||
if (editSavedQueryId?.length) {
|
||||
return <EditSavedQueryPage onSuccess={goBack} savedQueryId={editSavedQueryId} />;
|
||||
}
|
||||
|
||||
return <QueriesPage onNewClick={handleNewQueryClick} onEditClick={setEditSavedQueryId} />;
|
||||
};
|
||||
|
||||
export const Queries = React.memo(QueriesComponent);
|
32
x-pack/plugins/osquery/public/queries/new/index.tsx
Normal file
32
x-pack/plugins/osquery/public/queries/new/index.tsx
Normal 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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { useMutation } from 'react-query';
|
||||
|
||||
import { useKibana } from '../../common/lib/kibana';
|
||||
import { SavedQueryForm } from '../form';
|
||||
|
||||
interface NewSavedQueryPageProps {
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
const NewSavedQueryPageComponent: React.FC<NewSavedQueryPageProps> = ({ onSuccess }) => {
|
||||
const { http } = useKibana().services;
|
||||
|
||||
const createSavedQueryMutation = useMutation(
|
||||
(payload) => http.post(`/internal/osquery/saved_query`, { body: JSON.stringify(payload) }),
|
||||
{
|
||||
onSuccess,
|
||||
}
|
||||
);
|
||||
|
||||
// @ts-expect-error update types
|
||||
return <SavedQueryForm handleSubmit={createSavedQueryMutation.mutate} />;
|
||||
};
|
||||
|
||||
export const NewSavedQueryPage = React.memo(NewSavedQueryPageComponent);
|
244
x-pack/plugins/osquery/public/queries/queries/index.tsx
Normal file
244
x-pack/plugins/osquery/public/queries/queries/index.tsx
Normal file
|
@ -0,0 +1,244 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { map } from 'lodash/fp';
|
||||
import {
|
||||
EuiBasicTable,
|
||||
EuiButton,
|
||||
EuiButtonIcon,
|
||||
EuiCodeBlock,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiSpacer,
|
||||
RIGHT_ALIGNMENT,
|
||||
} from '@elastic/eui';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { useQuery, useQueryClient, useMutation } from 'react-query';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import qs from 'query-string';
|
||||
|
||||
import { useKibana } from '../../common/lib/kibana';
|
||||
|
||||
interface QueriesPageProps {
|
||||
onEditClick: (savedQueryId: string) => void;
|
||||
onNewClick: () => void;
|
||||
}
|
||||
|
||||
const QueriesPageComponent: React.FC<QueriesPageProps> = ({ onEditClick, onNewClick }) => {
|
||||
const { push } = useHistory();
|
||||
const queryClient = useQueryClient();
|
||||
const [pageIndex, setPageIndex] = useState(0);
|
||||
const [pageSize, setPageSize] = useState(5);
|
||||
const [sortField, setSortField] = useState('updated_at');
|
||||
const [sortDirection, setSortDirection] = useState('desc');
|
||||
const [selectedItems, setSelectedItems] = useState([]);
|
||||
const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState<Record<string, unknown>>({});
|
||||
const { http } = useKibana().services;
|
||||
|
||||
const deleteSavedQueriesMutation = useMutation(
|
||||
(payload) => http.delete(`/internal/osquery/saved_query`, { body: JSON.stringify(payload) }),
|
||||
{
|
||||
onSuccess: () => queryClient.invalidateQueries('savedQueryList'),
|
||||
}
|
||||
);
|
||||
|
||||
const { data = {} } = useQuery(
|
||||
['savedQueryList', { pageIndex, pageSize, sortField, sortDirection }],
|
||||
() =>
|
||||
http.get('/internal/osquery/saved_query', {
|
||||
query: {
|
||||
pageIndex,
|
||||
pageSize,
|
||||
sortField,
|
||||
sortDirection,
|
||||
},
|
||||
}),
|
||||
{
|
||||
keepPreviousData: true,
|
||||
// Refetch the data every 10 seconds
|
||||
refetchInterval: 5000,
|
||||
}
|
||||
);
|
||||
const { total = 0, saved_objects: savedQueries } = data;
|
||||
|
||||
const toggleDetails = useCallback(
|
||||
(item) => () => {
|
||||
const itemIdToExpandedRowMapValues = { ...itemIdToExpandedRowMap };
|
||||
if (itemIdToExpandedRowMapValues[item.id]) {
|
||||
delete itemIdToExpandedRowMapValues[item.id];
|
||||
} else {
|
||||
itemIdToExpandedRowMapValues[item.id] = (
|
||||
<EuiCodeBlock language="sql" fontSize="m" paddingSize="m">
|
||||
{item.attributes.query}
|
||||
</EuiCodeBlock>
|
||||
);
|
||||
}
|
||||
setItemIdToExpandedRowMap(itemIdToExpandedRowMapValues);
|
||||
},
|
||||
[itemIdToExpandedRowMap]
|
||||
);
|
||||
|
||||
const renderExtendedItemToggle = useCallback(
|
||||
(item) => (
|
||||
<EuiButtonIcon
|
||||
onClick={toggleDetails(item)}
|
||||
aria-label={itemIdToExpandedRowMap[item.id] ? 'Collapse' : 'Expand'}
|
||||
iconType={itemIdToExpandedRowMap[item.id] ? 'arrowUp' : 'arrowDown'}
|
||||
/>
|
||||
),
|
||||
[itemIdToExpandedRowMap, toggleDetails]
|
||||
);
|
||||
|
||||
const handleEditClick = useCallback((item) => onEditClick(item.id), [onEditClick]);
|
||||
|
||||
const handlePlayClick = useCallback(
|
||||
(item) =>
|
||||
push({
|
||||
search: qs.stringify({
|
||||
tab: 'live_query',
|
||||
}),
|
||||
state: {
|
||||
query: {
|
||||
id: item.id,
|
||||
query: item.attributes.query,
|
||||
},
|
||||
},
|
||||
}),
|
||||
[push]
|
||||
);
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{
|
||||
field: 'attributes.name',
|
||||
name: 'Query name',
|
||||
sortable: true,
|
||||
truncateText: true,
|
||||
},
|
||||
{
|
||||
field: 'attributes.description',
|
||||
name: 'Description',
|
||||
sortable: true,
|
||||
truncateText: true,
|
||||
},
|
||||
{
|
||||
field: 'updated_at',
|
||||
name: 'Last updated at',
|
||||
sortable: true,
|
||||
truncateText: true,
|
||||
},
|
||||
{
|
||||
name: 'Actions',
|
||||
actions: [
|
||||
{
|
||||
name: 'Live query',
|
||||
description: 'Run live query',
|
||||
type: 'icon',
|
||||
icon: 'play',
|
||||
onClick: handlePlayClick,
|
||||
},
|
||||
{
|
||||
name: 'Edit',
|
||||
description: 'Edit or run this query',
|
||||
type: 'icon',
|
||||
icon: 'documentEdit',
|
||||
onClick: handleEditClick,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
align: RIGHT_ALIGNMENT,
|
||||
width: '40px',
|
||||
isExpander: true,
|
||||
render: renderExtendedItemToggle,
|
||||
},
|
||||
],
|
||||
[handleEditClick, handlePlayClick, renderExtendedItemToggle]
|
||||
);
|
||||
|
||||
const onTableChange = useCallback(({ page = {}, sort = {} }) => {
|
||||
setPageIndex(page.index);
|
||||
setPageSize(page.size);
|
||||
setSortField(sort.field);
|
||||
setSortDirection(sort.direction);
|
||||
}, []);
|
||||
|
||||
const pagination = useMemo(
|
||||
() => ({
|
||||
pageIndex,
|
||||
pageSize,
|
||||
totalItemCount: total,
|
||||
pageSizeOptions: [3, 5, 8],
|
||||
}),
|
||||
[total, pageIndex, pageSize]
|
||||
);
|
||||
|
||||
const sorting = useMemo(
|
||||
() => ({
|
||||
sort: {
|
||||
field: sortField,
|
||||
direction: sortDirection,
|
||||
},
|
||||
}),
|
||||
[sortDirection, sortField]
|
||||
);
|
||||
|
||||
const selection = useMemo(
|
||||
() => ({
|
||||
selectable: () => true,
|
||||
onSelectionChange: setSelectedItems,
|
||||
initialSelected: [],
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
const handleDeleteClick = useCallback(() => {
|
||||
const selectedItemsIds = map<string>('id', selectedItems);
|
||||
// @ts-expect-error update types
|
||||
deleteSavedQueriesMutation.mutate({ savedQueryIds: selectedItemsIds });
|
||||
}, [deleteSavedQueriesMutation, selectedItems]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<EuiFlexGroup justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
{!selectedItems.length ? (
|
||||
<EuiButton fill onClick={onNewClick}>
|
||||
{'New query'}
|
||||
</EuiButton>
|
||||
) : (
|
||||
<EuiButton color="danger" iconType="trash" onClick={handleDeleteClick}>
|
||||
{`Delete ${selectedItems.length} Queries`}
|
||||
</EuiButton>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiSpacer />
|
||||
|
||||
{savedQueries && (
|
||||
<EuiBasicTable
|
||||
items={savedQueries}
|
||||
itemId="id"
|
||||
// @ts-expect-error update types
|
||||
columns={columns}
|
||||
pagination={pagination}
|
||||
// @ts-expect-error update types
|
||||
sorting={sorting}
|
||||
isSelectable={true}
|
||||
selection={selection}
|
||||
onChange={onTableChange}
|
||||
// @ts-expect-error update types
|
||||
itemIdToExpandedRowMap={itemIdToExpandedRowMap}
|
||||
rowHeader="id"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const QueriesPage = React.memo(QueriesPageComponent);
|
|
@ -6,20 +6,22 @@
|
|||
*/
|
||||
|
||||
import { isEmpty, isEqual, keys, map } from 'lodash/fp';
|
||||
import { EuiDataGrid, EuiDataGridProps, EuiDataGridColumn } from '@elastic/eui';
|
||||
import { EuiDataGrid, EuiDataGridProps, EuiDataGridColumn, EuiLink } 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';
|
||||
import { useRouterNavigate } from '../common/lib/kibana';
|
||||
|
||||
const DataContext = createContext<ResultEdges>([]);
|
||||
|
||||
interface ResultsTableComponentProps {
|
||||
actionId: string;
|
||||
agentId?: string;
|
||||
}
|
||||
|
||||
const ResultsTableComponent: React.FC<ResultsTableComponentProps> = ({ actionId }) => {
|
||||
const ResultsTableComponent: React.FC<ResultsTableComponentProps> = ({ actionId, agentId }) => {
|
||||
const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 50 });
|
||||
const onChangeItemsPerPage = useCallback(
|
||||
(pageSize) =>
|
||||
|
@ -46,8 +48,9 @@ const ResultsTableComponent: React.FC<ResultsTableComponentProps> = ({ actionId
|
|||
[setSortingColumns]
|
||||
);
|
||||
|
||||
const [, { results, totalCount }] = useAllResults({
|
||||
const { data: allResultsData = [] } = useAllResults({
|
||||
actionId,
|
||||
agentId,
|
||||
activePage: pagination.pageIndex,
|
||||
limit: pagination.pageSize,
|
||||
direction: Direction.asc,
|
||||
|
@ -61,15 +64,22 @@ const ResultsTableComponent: React.FC<ResultsTableComponentProps> = ({ actionId
|
|||
]);
|
||||
|
||||
const renderCellValue: EuiDataGridProps['renderCellValue'] = useMemo(
|
||||
() => ({ rowIndex, columnId, setCellProps }) => {
|
||||
() => ({ rowIndex, columnId }) => {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const data = useContext(DataContext);
|
||||
|
||||
const value = data[rowIndex].fields[columnId];
|
||||
|
||||
if (columnId === 'agent.name') {
|
||||
const agentIdValue = data[rowIndex].fields['agent.id'];
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const linkProps = useRouterNavigate(`/live_query/${actionId}/results/${agentIdValue}`);
|
||||
return <EuiLink {...linkProps}>{value}</EuiLink>;
|
||||
}
|
||||
|
||||
return !isEmpty(value) ? value : '-';
|
||||
},
|
||||
[]
|
||||
[actionId]
|
||||
);
|
||||
|
||||
const tableSorting = useMemo(() => ({ columns: sortingColumns, onSort }), [
|
||||
|
@ -88,30 +98,59 @@ const ResultsTableComponent: React.FC<ResultsTableComponentProps> = ({ actionId
|
|||
);
|
||||
|
||||
useEffect(() => {
|
||||
const newColumns: EuiDataGridColumn[] = keys(results[0]?.fields)
|
||||
// @ts-expect-error update types
|
||||
if (!allResultsData?.results) {
|
||||
return;
|
||||
}
|
||||
// @ts-expect-error update types
|
||||
const newColumns = keys(allResultsData?.results[0]?.fields)
|
||||
.sort()
|
||||
.map((fieldName) => ({
|
||||
id: fieldName,
|
||||
displayAsText: fieldName.split('.')[1],
|
||||
defaultSortDirection: 'asc',
|
||||
}));
|
||||
.reduce((acc, fieldName) => {
|
||||
if (fieldName === 'agent.name') {
|
||||
return [
|
||||
...acc,
|
||||
{
|
||||
id: fieldName,
|
||||
displayAsText: 'agent',
|
||||
defaultSortDirection: Direction.asc,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (fieldName.startsWith('osquery.')) {
|
||||
return [
|
||||
...acc,
|
||||
{
|
||||
id: fieldName,
|
||||
displayAsText: fieldName.split('.')[1],
|
||||
defaultSortDirection: Direction.asc,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, [] as EuiDataGridColumn[]);
|
||||
|
||||
if (!isEqual(columns, newColumns)) {
|
||||
setColumns(newColumns);
|
||||
setVisibleColumns(map('id', newColumns));
|
||||
}
|
||||
}, [columns, results]);
|
||||
// @ts-expect-error update types
|
||||
}, [columns, allResultsData?.results]);
|
||||
|
||||
return (
|
||||
<DataContext.Provider value={results}>
|
||||
// @ts-expect-error update types
|
||||
<DataContext.Provider value={allResultsData?.results}>
|
||||
<EuiDataGrid
|
||||
aria-label="Osquery results"
|
||||
columns={columns}
|
||||
columnVisibility={columnVisibility}
|
||||
rowCount={totalCount}
|
||||
// @ts-expect-error update types
|
||||
rowCount={allResultsData?.totalCount ?? 0}
|
||||
renderCellValue={renderCellValue}
|
||||
sorting={tableSorting}
|
||||
pagination={tablePagination}
|
||||
height="300px"
|
||||
/>
|
||||
</DataContext.Provider>
|
||||
);
|
||||
|
|
|
@ -6,14 +6,14 @@
|
|||
*/
|
||||
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
|
||||
import { createFilter } from '../common/helpers';
|
||||
import { useKibana } from '../common/lib/kibana';
|
||||
import {
|
||||
ResultEdges,
|
||||
PageInfoPaginated,
|
||||
DocValueFields,
|
||||
OsqueryQueries,
|
||||
ResultsRequestOptions,
|
||||
ResultsStrategyResponse,
|
||||
|
@ -21,13 +21,8 @@ import {
|
|||
} 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;
|
||||
|
@ -40,10 +35,10 @@ export interface ResultsArgs {
|
|||
interface UseAllResults {
|
||||
actionId: string;
|
||||
activePage: number;
|
||||
agentId?: string;
|
||||
direction: Direction;
|
||||
limit: number;
|
||||
sortField: string;
|
||||
docValueFields?: DocValueFields[];
|
||||
filterQuery?: ESTermQuery | string;
|
||||
skip?: boolean;
|
||||
}
|
||||
|
@ -51,89 +46,38 @@ interface UseAllResults {
|
|||
export const useAllResults = ({
|
||||
actionId,
|
||||
activePage,
|
||||
agentId,
|
||||
direction,
|
||||
limit,
|
||||
sortField,
|
||||
docValueFields,
|
||||
filterQuery,
|
||||
skip = false,
|
||||
}: UseAllResults): [boolean, ResultsArgs] => {
|
||||
const { data, notifications } = useKibana().services;
|
||||
}: UseAllResults) => {
|
||||
const { data } = 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 response = useQuery(
|
||||
['allActionResults', { actionId, activePage, direction, limit, sortField }],
|
||||
async () => {
|
||||
if (!resultsRequest) return Promise.resolve();
|
||||
|
||||
const resultsSearch = useCallback(
|
||||
(request: ResultsRequestOptions | null) => {
|
||||
if (request == null || skip) {
|
||||
return;
|
||||
}
|
||||
const responseData = await data.search
|
||||
.search<ResultsRequestOptions, ResultsStrategyResponse>(resultsRequest, {
|
||||
strategy: 'osquerySearchStrategy',
|
||||
})
|
||||
.toPromise();
|
||||
|
||||
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();
|
||||
return {
|
||||
...responseData,
|
||||
results: responseData.edges,
|
||||
inspect: getInspectResponse(responseData, {} as InspectResponse),
|
||||
};
|
||||
},
|
||||
[data.search, notifications.toasts, skip]
|
||||
{
|
||||
refetchInterval: 1000,
|
||||
enabled: !skip && !!resultsRequest,
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -141,7 +85,7 @@ export const useAllResults = ({
|
|||
const myRequest = {
|
||||
...(prevRequest ?? {}),
|
||||
actionId,
|
||||
docValueFields: docValueFields ?? [],
|
||||
agentId,
|
||||
factoryQueryType: OsqueryQueries.results,
|
||||
filterQuery: createFilter(filterQuery),
|
||||
pagination: generateTablePaginationOptions(activePage, limit),
|
||||
|
@ -155,11 +99,7 @@ export const useAllResults = ({
|
|||
}
|
||||
return prevRequest;
|
||||
});
|
||||
}, [actionId, activePage, direction, docValueFields, filterQuery, limit, sortField]);
|
||||
}, [actionId, activePage, agentId, direction, filterQuery, limit, sortField]);
|
||||
|
||||
useEffect(() => {
|
||||
resultsSearch(resultsRequest);
|
||||
}, [resultsRequest, resultsSearch]);
|
||||
|
||||
return [loading, resultsResponse];
|
||||
return response;
|
||||
};
|
||||
|
|
31
x-pack/plugins/osquery/public/routes/index.tsx
Normal file
31
x-pack/plugins/osquery/public/routes/index.tsx
Normal 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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Switch, Redirect, Route } from 'react-router-dom';
|
||||
|
||||
import { LiveQueries } from './live_query';
|
||||
|
||||
const OsqueryAppRoutesComponent = () => (
|
||||
<Switch>
|
||||
{/* <Route path="/packs">
|
||||
<Packs />
|
||||
</Route>
|
||||
<Route path={`/scheduled_queries`}>
|
||||
<ScheduledQueries />
|
||||
</Route>
|
||||
<Route path={`/queries`}>
|
||||
<Queries />
|
||||
</Route> */}
|
||||
<Route path="/live_query">
|
||||
<LiveQueries />
|
||||
</Route>
|
||||
<Redirect to="/live_query" />
|
||||
</Switch>
|
||||
);
|
||||
|
||||
export const OsqueryAppRoutes = React.memo(OsqueryAppRoutesComponent);
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import {
|
||||
EuiButtonEmpty,
|
||||
EuiText,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiCodeBlock,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { useRouterNavigate } from '../../../common/lib/kibana';
|
||||
import { WithHeaderLayout } from '../../../components/layouts';
|
||||
import { useActionDetails } from '../../../actions/use_action_details';
|
||||
import { ResultsTable } from '../../../results/results_table';
|
||||
|
||||
const LiveQueryAgentDetailsPageComponent = () => {
|
||||
const { actionId, agentId } = useParams<{ actionId: string; agentId: string }>();
|
||||
const { data } = useActionDetails({ actionId });
|
||||
const liveQueryListProps = useRouterNavigate(`live_query/${actionId}`);
|
||||
|
||||
const LeftColumn = useMemo(
|
||||
() => (
|
||||
<EuiFlexGroup alignItems="flexStart" direction="column" gutterSize="m">
|
||||
<EuiFlexItem>
|
||||
<EuiButtonEmpty iconType="arrowLeft" {...liveQueryListProps} flush="left" size="xs">
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.liveQueryAgentDetails.viewLiveQueryResultsTitle"
|
||||
defaultMessage="View all live query results"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiText>
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.liveQueryAgentDetails.pageTitle"
|
||||
defaultMessage="Live query {agentId} agent results"
|
||||
// eslint-disable-next-line react-perf/jsx-no-new-object-as-prop
|
||||
values={{ agentId }}
|
||||
/>
|
||||
</h1>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiText color="subdued">
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.liveQueryAgentDetails.pageSubtitle"
|
||||
defaultMessage="Lorem ipsum dolor sit amet, consectetur adipiscing elit."
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
[agentId, liveQueryListProps]
|
||||
);
|
||||
|
||||
return (
|
||||
<WithHeaderLayout leftColumn={LeftColumn}>
|
||||
<EuiCodeBlock language="sql" fontSize="m" paddingSize="m">
|
||||
{
|
||||
// @ts-expect-error update types
|
||||
data?.actionDetails._source?.data?.query
|
||||
}
|
||||
</EuiCodeBlock>
|
||||
<EuiSpacer />
|
||||
<ResultsTable actionId={actionId} agentId={agentId} />
|
||||
</WithHeaderLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export const LiveQueryAgentDetailsPage = React.memo(LiveQueryAgentDetailsPageComponent);
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiButton, EuiContextMenuPanel, EuiContextMenuItem, EuiPopover } from '@elastic/eui';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
|
||||
interface LiveQueryDetailsActionsMenuProps {
|
||||
actionId: string;
|
||||
}
|
||||
|
||||
const LiveQueryDetailsActionsMenuComponent: React.FC<LiveQueryDetailsActionsMenuProps> = ({
|
||||
actionId,
|
||||
}) => {
|
||||
const services = useKibana().services;
|
||||
const [isPopoverOpen, setPopover] = useState(false);
|
||||
|
||||
const discoverLinkHref = services?.application?.getUrlForApp('discover', {
|
||||
path: `#/?_g=(filters:!(),refreshInterval:(pause:!t,value:0),time:(from:now-24h,to:now))&_a=(columns:!(),filters:!(('$state':(store:appState),meta:(alias:!n,disabled:!f,index:'logs-*',key:action_id,negate:!f,params:(query:'${actionId}'),type:phrase),query:(match_phrase:(action_id:'${actionId}')))),index:'logs-*',interval:auto,query:(language:kuery,query:''),sort:!(!('@timestamp',desc)))`,
|
||||
});
|
||||
|
||||
const onButtonClick = useCallback(() => {
|
||||
setPopover((currentIsPopoverOpen) => !currentIsPopoverOpen);
|
||||
}, []);
|
||||
|
||||
const closePopover = useCallback(() => {
|
||||
setPopover(false);
|
||||
}, []);
|
||||
|
||||
const items = useMemo(
|
||||
() => [
|
||||
<EuiContextMenuItem key="copy" icon="copy" href={discoverLinkHref}>
|
||||
Check results in Discover
|
||||
</EuiContextMenuItem>,
|
||||
],
|
||||
[discoverLinkHref]
|
||||
);
|
||||
|
||||
const button = (
|
||||
<EuiButton iconType="arrowDown" iconSide="right" onClick={onButtonClick}>
|
||||
Actions
|
||||
</EuiButton>
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
id="liveQueryDetailsActionsMenu"
|
||||
button={button}
|
||||
isOpen={isPopoverOpen}
|
||||
closePopover={closePopover}
|
||||
panelPaddingSize="none"
|
||||
>
|
||||
<EuiContextMenuPanel size="s" items={items} />
|
||||
</EuiPopover>
|
||||
);
|
||||
};
|
||||
|
||||
export const LiveQueryDetailsActionsMenu = React.memo(LiveQueryDetailsActionsMenuComponent);
|
|
@ -0,0 +1,167 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import {
|
||||
EuiButtonEmpty,
|
||||
EuiText,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiCodeBlock,
|
||||
EuiSpacer,
|
||||
EuiDescriptionList,
|
||||
EuiDescriptionListTitle,
|
||||
EuiDescriptionListDescription,
|
||||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { Direction } from '../../../../common/search_strategy';
|
||||
import { useRouterNavigate } from '../../../common/lib/kibana';
|
||||
import { WithHeaderLayout } from '../../../components/layouts';
|
||||
import { useActionResults } from '../../../action_results/use_action_results';
|
||||
import { useActionDetails } from '../../../actions/use_action_details';
|
||||
import { ResultTabs } from '../../../queries/edit/tabs';
|
||||
import { LiveQueryDetailsActionsMenu } from './actions_menu';
|
||||
|
||||
const Divider = styled.div`
|
||||
width: 0;
|
||||
height: 100%;
|
||||
border-left: ${({ theme }) => theme.eui.euiBorderThin};
|
||||
`;
|
||||
|
||||
const LiveQueryDetailsPageComponent = () => {
|
||||
const { actionId } = useParams<{ actionId: string }>();
|
||||
const liveQueryListProps = useRouterNavigate('live_query');
|
||||
|
||||
const { data } = useActionDetails({ actionId });
|
||||
const { data: actionResultsData } = useActionResults({
|
||||
actionId,
|
||||
activePage: 0,
|
||||
limit: 0,
|
||||
direction: Direction.asc,
|
||||
sortField: '@timestamp',
|
||||
});
|
||||
|
||||
const LeftColumn = useMemo(
|
||||
() => (
|
||||
<EuiFlexGroup alignItems="flexStart" direction="column" gutterSize="m">
|
||||
<EuiFlexItem>
|
||||
<EuiButtonEmpty iconType="arrowLeft" {...liveQueryListProps} flush="left" size="xs">
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.liveQueryDetails.viewLiveQueriesListTitle"
|
||||
defaultMessage="View all live queries"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiText>
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.liveQueryDetails.pageTitle"
|
||||
defaultMessage="Live query results"
|
||||
/>
|
||||
</h1>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiText color="subdued">
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.liveQueryDetails.pageSubtitle"
|
||||
defaultMessage="Lorem ipsum dolor sit amet, consectetur adipiscing elit."
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
[liveQueryListProps]
|
||||
);
|
||||
|
||||
const RightColumn = useMemo(
|
||||
() => (
|
||||
<EuiFlexGroup justifyContent="flexEnd" direction="row">
|
||||
<EuiFlexItem grow={false} key="rows_count">
|
||||
<></>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false} key="rows_count_divider">
|
||||
<Divider />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false} key="agents_count">
|
||||
{/* eslint-disable-next-line react-perf/jsx-no-new-object-as-prop */}
|
||||
<EuiDescriptionList compressed textStyle="reverse" style={{ textAlign: 'right' }}>
|
||||
<EuiDescriptionListTitle className="eui-textNoWrap">
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.liveQueryDetails.kpis.agentsQueriedLabelText"
|
||||
defaultMessage="Agents queried"
|
||||
/>
|
||||
</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription className="eui-textNoWrap">
|
||||
{
|
||||
// @ts-expect-error update types
|
||||
data?.actionDetails?.fields?.agents?.length ?? '0'
|
||||
}
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiDescriptionList>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false} key="agents_count_divider">
|
||||
<Divider />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false} key="agents_failed_count">
|
||||
{/* eslint-disable-next-line react-perf/jsx-no-new-object-as-prop */}
|
||||
<EuiDescriptionList compressed textStyle="reverse" style={{ textAlign: 'right' }}>
|
||||
<EuiDescriptionListTitle className="eui-textNoWrap">
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.liveQueryDetails.kpis.agentsFailedCountLabelText"
|
||||
defaultMessage="Agents failed"
|
||||
/>
|
||||
</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription className="eui-textNoWrap">
|
||||
{
|
||||
// @ts-expect-error update types
|
||||
actionResultsData?.rawResponse?.aggregations?.responses?.buckets.find(
|
||||
// @ts-expect-error update types
|
||||
(bucket) => bucket.key === 'error'
|
||||
)?.doc_count ?? '0'
|
||||
}
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiDescriptionList>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false} key="agents_count_divider">
|
||||
<Divider />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false} key="actions_menu">
|
||||
<LiveQueryDetailsActionsMenu actionId={actionId} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
[
|
||||
actionId,
|
||||
// @ts-expect-error update types
|
||||
actionResultsData?.rawResponse?.aggregations?.responses?.buckets,
|
||||
// @ts-expect-error update types
|
||||
data?.actionDetails?.fields?.agents?.length,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
<WithHeaderLayout leftColumn={LeftColumn} rightColumn={RightColumn} rightColumnGrow={false}>
|
||||
<EuiCodeBlock language="sql" fontSize="m" paddingSize="m">
|
||||
{
|
||||
// @ts-expect-error update types
|
||||
data?.actionDetails._source?.data?.query
|
||||
}
|
||||
</EuiCodeBlock>
|
||||
<EuiSpacer />
|
||||
<ResultTabs actionId={actionId} />
|
||||
</WithHeaderLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export const LiveQueryDetailsPage = React.memo(LiveQueryDetailsPageComponent);
|
37
x-pack/plugins/osquery/public/routes/live_query/index.tsx
Normal file
37
x-pack/plugins/osquery/public/routes/live_query/index.tsx
Normal 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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Switch, Route, useRouteMatch } from 'react-router-dom';
|
||||
|
||||
import { LiveQueriesPage } from './list';
|
||||
import { NewLiveQueryPage } from './new';
|
||||
import { LiveQueryDetailsPage } from './details';
|
||||
import { LiveQueryAgentDetailsPage } from './agent_details';
|
||||
|
||||
const LiveQueriesComponent = () => {
|
||||
const match = useRouteMatch();
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
<Route path={`${match.url}/new`}>
|
||||
<NewLiveQueryPage />
|
||||
</Route>
|
||||
<Route path={`${match.url}/:actionId/results/:agentId`}>
|
||||
<LiveQueryAgentDetailsPage />
|
||||
</Route>
|
||||
<Route path={`${match.url}/:actionId`}>
|
||||
<LiveQueryDetailsPage />
|
||||
</Route>
|
||||
<Route path={`${match.url}`}>
|
||||
<LiveQueriesPage />
|
||||
</Route>
|
||||
</Switch>
|
||||
);
|
||||
};
|
||||
|
||||
export const LiveQueries = React.memo(LiveQueriesComponent);
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiButton, EuiText, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { useRouterNavigate } from '../../../common/lib/kibana';
|
||||
import { ActionsTable } from '../../../actions/actions_table';
|
||||
import { WithHeaderLayout } from '../../../components/layouts';
|
||||
|
||||
const LiveQueriesPageComponent = () => {
|
||||
const newQueryLinkProps = useRouterNavigate('live_query/new');
|
||||
|
||||
const LeftColumn = useMemo(
|
||||
() => (
|
||||
<EuiFlexGroup direction="column" gutterSize="m">
|
||||
<EuiFlexItem>
|
||||
<EuiText>
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.liveQueryList.pageTitle"
|
||||
defaultMessage="Live queries"
|
||||
/>
|
||||
</h1>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiText color="subdued">
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.liveQueryList.pageSubtitle"
|
||||
defaultMessage="Lorem ipsum dolor sit amet, consectetur adipiscing elit."
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
[]
|
||||
);
|
||||
|
||||
const RightColumn = useMemo(
|
||||
() => (
|
||||
<EuiButton fill {...newQueryLinkProps}>
|
||||
{'New live query'}
|
||||
</EuiButton>
|
||||
),
|
||||
[newQueryLinkProps]
|
||||
);
|
||||
|
||||
return (
|
||||
<WithHeaderLayout leftColumn={LeftColumn} rightColumn={RightColumn} rightColumnGrow={false}>
|
||||
<ActionsTable />
|
||||
</WithHeaderLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export const LiveQueriesPage = React.memo(LiveQueriesPageComponent);
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiButtonEmpty, EuiText, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { WithHeaderLayout } from '../../../components/layouts';
|
||||
import { useRouterNavigate } from '../../../common/lib/kibana';
|
||||
import { LiveQuery } from '../../../live_query';
|
||||
|
||||
const NewLiveQueryPageComponent = () => {
|
||||
const liveQueryListProps = useRouterNavigate('live_query');
|
||||
|
||||
const LeftColumn = useMemo(
|
||||
() => (
|
||||
<EuiFlexGroup alignItems="flexStart" direction="column" gutterSize="m">
|
||||
<EuiFlexItem>
|
||||
<EuiButtonEmpty iconType="arrowLeft" {...liveQueryListProps} flush="left" size="xs">
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.newLiveQuery.viewLiveQueriesListTitle"
|
||||
defaultMessage="View all live queries"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiText>
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.newLiveQuery.pageTitle"
|
||||
defaultMessage="New Live query"
|
||||
/>
|
||||
</h1>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiText color="subdued">
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.osquery.newLiveQuery.pageSubtitle"
|
||||
defaultMessage="Lorem ipsum dolor sit amet, consectetur adipiscing elit."
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
[liveQueryListProps]
|
||||
);
|
||||
|
||||
return (
|
||||
<WithHeaderLayout leftColumn={LeftColumn}>
|
||||
<LiveQuery />
|
||||
</WithHeaderLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export const NewLiveQueryPage = React.memo(NewLiveQueryPageComponent);
|
|
@ -0,0 +1,169 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { find } from 'lodash/fp';
|
||||
import {
|
||||
EuiButtonIcon,
|
||||
EuiFieldText,
|
||||
EuiForm,
|
||||
EuiFormRow,
|
||||
EuiSelect,
|
||||
EuiSpacer,
|
||||
EuiSwitch,
|
||||
EuiHorizontalRule,
|
||||
} from '@elastic/eui';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
|
||||
import { useKibana } from '../../common/lib/kibana';
|
||||
|
||||
// @ts-expect-error update types
|
||||
const OsqueryStreamFieldComponent = ({ field, removeItem }) => {
|
||||
const { http } = useKibana().services;
|
||||
const { data: { saved_objects: savedQueries } = {} } = useQuery(['savedQueryList'], () =>
|
||||
http.get('/internal/osquery/saved_query', {
|
||||
query: { pageIndex: 0, pageSize: 100, sortField: 'updated_at', sortDirection: 'desc' },
|
||||
})
|
||||
);
|
||||
|
||||
const { setValue } = field;
|
||||
|
||||
const savedQueriesOptions = useMemo(
|
||||
() =>
|
||||
// @ts-expect-error update types
|
||||
(savedQueries ?? []).map((savedQuery) => ({
|
||||
text: savedQuery.attributes.name,
|
||||
value: savedQuery.id,
|
||||
})),
|
||||
[savedQueries]
|
||||
);
|
||||
|
||||
const handleSavedQueryChange = useCallback(
|
||||
(event) => {
|
||||
event.persist();
|
||||
const savedQueryId = event.target.value;
|
||||
const savedQuery = find(['id', savedQueryId], savedQueries);
|
||||
|
||||
if (savedQuery) {
|
||||
// @ts-expect-error update types
|
||||
setValue((prev) => ({
|
||||
...prev,
|
||||
vars: {
|
||||
...prev.vars,
|
||||
id: {
|
||||
...prev.vars.id,
|
||||
value: savedQuery.id,
|
||||
},
|
||||
query: {
|
||||
...prev.vars.query,
|
||||
value: savedQuery.attributes.query,
|
||||
},
|
||||
},
|
||||
}));
|
||||
}
|
||||
},
|
||||
[savedQueries, setValue]
|
||||
);
|
||||
|
||||
const handleEnabledChange = useCallback(() => {
|
||||
// @ts-expect-error update types
|
||||
setValue((prev) => ({
|
||||
...prev,
|
||||
enabled: !prev.enabled,
|
||||
}));
|
||||
}, [setValue]);
|
||||
|
||||
const handleQueryChange = useCallback(
|
||||
(event) => {
|
||||
event.persist();
|
||||
// @ts-expect-error update types
|
||||
setValue((prev) => ({
|
||||
...prev,
|
||||
vars: {
|
||||
...prev.vars,
|
||||
query: {
|
||||
...prev.vars.query,
|
||||
value: event.target.value,
|
||||
},
|
||||
},
|
||||
}));
|
||||
},
|
||||
[setValue]
|
||||
);
|
||||
|
||||
const handleIntervalChange = useCallback(
|
||||
(event) => {
|
||||
event.persist();
|
||||
// @ts-expect-error update types
|
||||
setValue((prev) => ({
|
||||
...prev,
|
||||
vars: {
|
||||
...prev.vars,
|
||||
interval: {
|
||||
...prev.vars.interval,
|
||||
value: event.target.value,
|
||||
},
|
||||
},
|
||||
}));
|
||||
},
|
||||
[setValue]
|
||||
);
|
||||
|
||||
const handleIdChange = useCallback(
|
||||
(event) => {
|
||||
event.persist();
|
||||
// @ts-expect-error update types
|
||||
setValue((prev) => ({
|
||||
...prev,
|
||||
vars: {
|
||||
...prev.vars,
|
||||
id: {
|
||||
...prev.vars.id,
|
||||
value: event.target.value,
|
||||
},
|
||||
},
|
||||
}));
|
||||
},
|
||||
[setValue]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiForm>
|
||||
<EuiFormRow>
|
||||
<EuiSwitch label="Enabled" checked={field.value.enabled} onChange={handleEnabledChange} />
|
||||
</EuiFormRow>
|
||||
<EuiSpacer />
|
||||
<EuiFormRow>
|
||||
<EuiButtonIcon aria-label="remove" onClick={removeItem} color="danger" iconType="trash" />
|
||||
</EuiFormRow>
|
||||
<EuiFormRow>
|
||||
<EuiSelect
|
||||
value={field.value.vars.id.value}
|
||||
hasNoInitialSelection
|
||||
options={savedQueriesOptions}
|
||||
onChange={handleSavedQueryChange}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiSpacer />
|
||||
<EuiFormRow>
|
||||
<EuiFieldText value={field.value.vars.query.value} onChange={handleQueryChange} />
|
||||
</EuiFormRow>
|
||||
<EuiSpacer />
|
||||
<EuiFormRow>
|
||||
<EuiFieldText value={field.value.vars.interval.value} onChange={handleIntervalChange} />
|
||||
</EuiFormRow>
|
||||
<EuiSpacer />
|
||||
<EuiFormRow>
|
||||
<EuiFieldText value={field.value.vars.id.value} onChange={handleIdChange} />
|
||||
</EuiFormRow>
|
||||
<EuiSpacer />
|
||||
<EuiHorizontalRule />
|
||||
</EuiForm>
|
||||
);
|
||||
};
|
||||
|
||||
export const OsqueryStreamField = React.memo(OsqueryStreamFieldComponent);
|
153
x-pack/plugins/osquery/public/scheduled_query/edit/form.tsx
Normal file
153
x-pack/plugins/osquery/public/scheduled_query/edit/form.tsx
Normal file
|
@ -0,0 +1,153 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import produce from 'immer';
|
||||
import { get, omit } from 'lodash/fp';
|
||||
import { EuiButton, EuiButtonEmpty, EuiSpacer, EuiHorizontalRule } from '@elastic/eui';
|
||||
import uuid from 'uuid';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import {
|
||||
UseField,
|
||||
useForm,
|
||||
UseArray,
|
||||
getUseField,
|
||||
Field,
|
||||
ToggleField,
|
||||
Form,
|
||||
} from '../../shared_imports';
|
||||
|
||||
import { OsqueryStreamField } from '../common/osquery_stream_field';
|
||||
import { schema } from './schema';
|
||||
|
||||
const CommonUseField = getUseField({ component: Field });
|
||||
|
||||
const EDIT_SCHEDULED_QUERY_FORM_ID = 'editScheduledQueryForm';
|
||||
|
||||
interface EditScheduledQueryFormProps {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
agentPolicies: Array<Record<string, any>>;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
data: Array<Record<string, any>>;
|
||||
handleSubmit: () => Promise<void>;
|
||||
}
|
||||
|
||||
const EditScheduledQueryFormComponent: React.FC<EditScheduledQueryFormProps> = ({
|
||||
agentPolicies,
|
||||
data,
|
||||
handleSubmit,
|
||||
}) => {
|
||||
const agentPoliciesOptions = useMemo(
|
||||
() =>
|
||||
agentPolicies.map((policy) => ({
|
||||
value: policy.id,
|
||||
text: policy.name,
|
||||
})),
|
||||
[agentPolicies]
|
||||
);
|
||||
|
||||
const { form } = useForm({
|
||||
schema,
|
||||
id: EDIT_SCHEDULED_QUERY_FORM_ID,
|
||||
onSubmit: handleSubmit,
|
||||
defaultValue: data,
|
||||
// @ts-expect-error update types
|
||||
deserializer: (payload) => {
|
||||
const deserialized = produce(payload, (draft) => {
|
||||
// @ts-expect-error update types
|
||||
draft.inputs[0].streams.forEach((stream) => {
|
||||
delete stream.compiled_stream;
|
||||
});
|
||||
});
|
||||
|
||||
return deserialized;
|
||||
},
|
||||
// @ts-expect-error update types
|
||||
serializer: (payload) =>
|
||||
omit(['id', 'revision', 'created_at', 'created_by', 'updated_at', 'updated_by', 'version'], {
|
||||
...data,
|
||||
...payload,
|
||||
// @ts-expect-error update types
|
||||
inputs: [{ type: 'osquery', ...((payload.inputs && payload.inputs[0]) ?? {}) }],
|
||||
}),
|
||||
});
|
||||
|
||||
const { submit } = form;
|
||||
|
||||
const policyIdComponentProps = useMemo(
|
||||
() => ({
|
||||
euiFieldProps: {
|
||||
disabled: true,
|
||||
options: agentPoliciesOptions,
|
||||
},
|
||||
}),
|
||||
[agentPoliciesOptions]
|
||||
);
|
||||
|
||||
return (
|
||||
<Form form={form}>
|
||||
<CommonUseField path="policy_id" componentProps={policyIdComponentProps} />
|
||||
<EuiSpacer />
|
||||
<CommonUseField path="name" />
|
||||
<EuiSpacer />
|
||||
<CommonUseField path="description" />
|
||||
<EuiSpacer />
|
||||
<CommonUseField path="inputs[0].enabled" component={ToggleField} />
|
||||
<EuiHorizontalRule />
|
||||
<EuiSpacer />
|
||||
<UseArray path="inputs[0].streams">
|
||||
{({ items, addItem, removeItem }) => (
|
||||
<>
|
||||
{items.map((item) => (
|
||||
<UseField
|
||||
key={item.path}
|
||||
path={item.path}
|
||||
component={OsqueryStreamField}
|
||||
// eslint-disable-next-line react/jsx-no-bind, react-perf/jsx-no-new-function-as-prop
|
||||
removeItem={() => removeItem(item.id)}
|
||||
defaultValue={
|
||||
// eslint-disable-next-line react-perf/jsx-no-new-object-as-prop
|
||||
get(item.path, form.getFormData()) ?? {
|
||||
data_stream: {
|
||||
type: 'logs',
|
||||
dataset: 'osquery_elastic_managed.osquery',
|
||||
},
|
||||
vars: {
|
||||
query: {
|
||||
type: 'text',
|
||||
value: 'select * from uptime',
|
||||
},
|
||||
interval: {
|
||||
type: 'text',
|
||||
value: '120',
|
||||
},
|
||||
id: {
|
||||
type: 'text',
|
||||
value: uuid.v4(),
|
||||
},
|
||||
},
|
||||
enabled: true,
|
||||
}
|
||||
}
|
||||
/>
|
||||
))}
|
||||
<EuiButtonEmpty onClick={addItem} iconType="plusInCircleFilled">
|
||||
{'Add query'}
|
||||
</EuiButtonEmpty>
|
||||
</>
|
||||
)}
|
||||
</UseArray>
|
||||
<EuiHorizontalRule />
|
||||
<EuiSpacer />
|
||||
<EuiButton fill onClick={submit}>
|
||||
Save
|
||||
</EuiButton>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export const EditScheduledQueryForm = React.memo(EditScheduledQueryFormComponent);
|
48
x-pack/plugins/osquery/public/scheduled_query/edit/index.tsx
Normal file
48
x-pack/plugins/osquery/public/scheduled_query/edit/index.tsx
Normal 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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useMutation, useQuery } from 'react-query';
|
||||
|
||||
import { useKibana } from '../../common/lib/kibana';
|
||||
import { EditScheduledQueryForm } from './form';
|
||||
|
||||
const EditScheduledQueryPageComponent = () => {
|
||||
const { http } = useKibana().services;
|
||||
const { scheduledQueryId } = useParams<{ scheduledQueryId: string }>();
|
||||
|
||||
const { data } = useQuery(['scheduledQuery', { scheduledQueryId }], () =>
|
||||
http.get(`/internal/osquery/scheduled_query/${scheduledQueryId}`)
|
||||
);
|
||||
|
||||
const { data: agentPolicies } = useQuery(
|
||||
['agentPolicy'],
|
||||
() => http.get(`/api/fleet/agent_policies`),
|
||||
{ initialData: { items: [] } }
|
||||
);
|
||||
|
||||
const updateScheduledQueryMutation = useMutation((payload) =>
|
||||
http.put(`/api/fleet/package_policies/${scheduledQueryId}`, { body: JSON.stringify(payload) })
|
||||
);
|
||||
|
||||
if (data) {
|
||||
return (
|
||||
<EditScheduledQueryForm
|
||||
data={data}
|
||||
// @ts-expect-error update types
|
||||
agentPolicies={agentPolicies?.items}
|
||||
// @ts-expect-error update types
|
||||
handleSubmit={updateScheduledQueryMutation.mutate}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <div>Loading</div>;
|
||||
};
|
||||
|
||||
export const EditScheduledQueryPage = React.memo(EditScheduledQueryPageComponent);
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue