Skip to content

Commit

Permalink
[Asset Management] Add support for Live queries in Osquery (#89889) (#…
Browse files Browse the repository at this point in the history
…94078)

Co-authored-by: Patryk Kopyciński <[email protected]>
  • Loading branch information
kibanamachine and patrykkopycinski authored Mar 9, 2021
1 parent 198d89d commit 274a649
Show file tree
Hide file tree
Showing 146 changed files with 7,309 additions and 795 deletions.
9 changes: 7 additions & 2 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -1292,14 +1292,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',
},
},
{
Expand Down
10 changes: 5 additions & 5 deletions api_docs/osquery.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"children": [],
"source": {
"path": "x-pack/plugins/osquery/public/types.ts",
"lineNumber": 14
"lineNumber": 18
},
"lifecycle": "setup",
"initialIsOpen": true
Expand All @@ -30,7 +30,7 @@
"children": [],
"source": {
"path": "x-pack/plugins/osquery/public/types.ts",
"lineNumber": 16
"lineNumber": 20
},
"lifecycle": "start",
"initialIsOpen": true
Expand All @@ -52,7 +52,7 @@
"children": [],
"source": {
"path": "x-pack/plugins/osquery/server/types.ts",
"lineNumber": 15
"lineNumber": 16
},
"lifecycle": "setup",
"initialIsOpen": true
Expand All @@ -66,7 +66,7 @@
"children": [],
"source": {
"path": "x-pack/plugins/osquery/server/types.ts",
"lineNumber": 17
"lineNumber": 18
},
"lifecycle": "start",
"initialIsOpen": true
Expand Down Expand Up @@ -134,7 +134,7 @@
"lineNumber": 11
},
"signature": [
"\"osquery\""
"\"Osquery\""
],
"initialIsOpen": false
}
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,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",
Expand Down
177 changes: 177 additions & 0 deletions x-pack/plugins/osquery/common/exact_check.test.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 93 additions & 0 deletions x-pack/plugins/osquery/common/exact_check.ts
Original file line number Diff line number Diff line change
@@ -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;
}
};
Loading

0 comments on commit 274a649

Please sign in to comment.