Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SecuritySolution][EntityAnalytics] Risk Scoring Preview API #155966

Merged
merged 103 commits into from
Jun 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
103 commits
Select commit Hold shift + click to select a range
46ac1b4
Add basic route for requesting risk scores
rylnd Feb 28, 2023
6f27d82
Fleshing out the logic of calculating risk scores
rylnd Mar 1, 2023
62c660e
Add typings and simple version of top (riskiest) inputs
rylnd Mar 1, 2023
a089398
Camel case on the outgoing response
rylnd Mar 2, 2023
bd9f562
Add ability to return full risk inputs based on a request parameter
rylnd Mar 2, 2023
26cafcd
Adds helper function for retrieval of input documents
rylnd Mar 2, 2023
16a0d5e
Respect filters param in endpoint
rylnd Mar 2, 2023
d2d81ba
Request risk inputs via current user
rylnd Mar 6, 2023
cd1e5b1
Adds raw score and risk level to response
rylnd Mar 7, 2023
cf86293
Endpoint now respects the `range` parameters
rylnd Mar 7, 2023
96481be
Limit the number of risk inputs processed to the first million
rylnd Mar 7, 2023
9f0080b
Add scoring notes to the response
rylnd Mar 7, 2023
ebea202
Return an error response if our scoring query blows up
rylnd Mar 7, 2023
4223ba2
Add note from marshall about potential performance improvement
rylnd Mar 7, 2023
b35f3e5
Filters passed via the API are a single object
rylnd Mar 7, 2023
229e1af
Rename parameter to reflect (lack of) plurality
rylnd Mar 7, 2023
1b49fc3
Retrieve the alerts index via our app client
rylnd Mar 8, 2023
715d8c8
Retrieve the index pattern from the provided dataViewId
rylnd Mar 8, 2023
c05497b
Exclude _source from top-level risk score query
rylnd Mar 8, 2023
bbda87e
Respect identifier_type parameter in UI
rylnd Mar 8, 2023
5393610
Add "debug" mode to risk score API
rylnd Mar 9, 2023
4b9447a
Log the request/response to kibana in debug mode
rylnd Mar 9, 2023
66c6afe
Better error handling
rylnd Mar 9, 2023
0edf2af
Log at the info level
rylnd Mar 9, 2023
197e749
Attempt to instrument our scoring calculation
rylnd Mar 9, 2023
3884985
Return the max number of identities for the implementation
rylnd Mar 9, 2023
76a2b9b
Tests for risk score
nkhristinin Mar 22, 2023
765e0a3
Add more tests
nkhristinin Mar 24, 2023
60d5e41
Decompose test helper function into component pieces
rylnd Mar 24, 2023
45f2846
Remove obfuscating helper function
rylnd Mar 24, 2023
527fb50
Only generate a uuid if we need it
rylnd Mar 24, 2023
9b395f7
Rename helper method
rylnd Mar 24, 2023
a6b4826
Adds initial risk weight calculation
rylnd Mar 24, 2023
9d132fb
Convert Risk Score aggregation to composite agg
rylnd Mar 28, 2023
e969fc2
Weights are specified as an array
rylnd Mar 29, 2023
e2cb1ad
Add order-dependent integration test
rylnd Mar 29, 2023
4668975
Update usage of test helper
rylnd Apr 19, 2023
54c0828
Update risk engine integration test descriptions
rylnd Apr 19, 2023
08bfacb
Update score fields to include category scores
rylnd Apr 21, 2023
dd229d4
WIP: Applying category weights to our scoring aggregation
rylnd Apr 21, 2023
bb951b0
Sort risk inputs by their category-weighted scores
rylnd Apr 21, 2023
a1c8e91
Address persistence of signals by scoping our scoring
rylnd Apr 21, 2023
b192bb4
Revert "Address persistence of signals by scoping our scoring"
rylnd Apr 21, 2023
31711c7
Delete documents instead of the index
rylnd Apr 21, 2023
154ccc5
Finalize initial category weight test
rylnd Apr 21, 2023
2965612
Add new client method to mock object
rylnd May 1, 2023
3c47ed7
Add some tests for our risk score route
rylnd May 1, 2023
ceb5ab8
Add tests around calculateRiskScores function
rylnd May 9, 2023
59b7c40
Update ticket to respect new alert limit
rylnd May 15, 2023
5cb1564
Merge branch 'main' into risk_score_api
rylnd May 15, 2023
f9a6e84
Define a parameter for the size of our terms' aggregation
rylnd May 16, 2023
80819ee
Allow pagination of the risk score endpoint
rylnd May 16, 2023
432c04d
Add integration tests for pagination and filtering
rylnd May 16, 2023
77414dd
Merge branch 'main' into risk_score_api
rylnd May 16, 2023
974795c
Merge branch 'main' into risk_score_api
rylnd May 17, 2023
350fe36
Add stricter request schema to remove need for type cast
rylnd May 17, 2023
ba949c8
Return top 10 riskiest inputs
rylnd May 17, 2023
c395c52
Add initial draft of OpenAPI spec for Risk scoring route
rylnd May 18, 2023
956b866
Document risk score response schema
rylnd May 18, 2023
194886a
Merge branch 'main' into risk_score_api
rylnd May 18, 2023
d74757b
Fix type error
rylnd May 19, 2023
5836677
Merge branch 'main' into risk_score_api
rylnd May 22, 2023
ed9075c
Add descriptions to most of our endpoint specification
rylnd May 22, 2023
33a6de2
Fix issue with $ref removing sibling keys
rylnd May 22, 2023
91c238d
Merge branch 'main' into risk_score_api
rylnd May 22, 2023
7172601
Merge branch 'main' into risk_score_api
rylnd May 23, 2023
6aea611
Remove pending route tests
rylnd May 23, 2023
7d8d15f
More accurate parameter name/value
rylnd May 23, 2023
419f9e0
Refactor construction of our identity aggregations
rylnd May 23, 2023
fd00984
More generic parameter name, exposed as API param
rylnd May 23, 2023
7b3e360
Remove unused type
rylnd May 23, 2023
a1457b1
Remove unused response schema
rylnd May 23, 2023
20395b7
Merge branch 'main' into risk_score_api
rylnd May 23, 2023
ac55ca0
Remove temporary test helper in favor of permanent upstream one
rylnd May 23, 2023
7574f54
Filter parameter is an object, not an array
rylnd May 26, 2023
0b17b54
Move endpoint location and naming to reflect its 'preview' behavior
rylnd May 26, 2023
9d8b193
Updates feature flag to be more general/accurate
rylnd May 26, 2023
86d786d
Update API spec to reflect the 'preview' orientation
rylnd May 26, 2023
097460e
Fix outdated import/constant
rylnd May 26, 2023
99ab426
Merge branch 'main' into risk_score_api
rylnd May 26, 2023
c1b4048
Remove accidentally-committed notes
rylnd May 26, 2023
c367435
Merge branch 'main' into risk_score_api
rylnd May 30, 2023
fde5717
Merge branch 'main' into risk_score_api
rylnd Jun 2, 2023
a4b7760
Remove auxiliary helper function
rylnd Jun 2, 2023
28cd88e
Adds a type representing probabilities/weights
rylnd Jun 6, 2023
6a9321a
Export and use our new io-ts type for weight values
rylnd Jun 7, 2023
69d21aa
Replace some special constants with enums
rylnd Jun 7, 2023
15591b8
Test stricter weight param validation
rylnd Jun 7, 2023
7aec3de
Rename request schema for accuracy
rylnd Jun 7, 2023
7dd7266
More strict schema validations on weighting
rylnd Jun 12, 2023
2448f1c
Merge branch 'main' into risk_score_api
rylnd Jun 12, 2023
302a153
Adds a few more tests around allowed 'type's and 'value's
rylnd Jun 12, 2023
c53a7ed
Fix imports
rylnd Jun 13, 2023
830edeb
Use our new schema in the API
rylnd Jun 13, 2023
6dcce07
Replace our static type for risk weights with the io-ts type
rylnd Jun 13, 2023
64e5c1d
Updates our identifierWeights schema
rylnd Jun 13, 2023
0d6c252
Move IdentifierType definition to one from io-ts
rylnd Jun 13, 2023
fc98b91
Move after_keys to io-ts
rylnd Jun 13, 2023
f6671bb
Some reorganization for consistency
rylnd Jun 13, 2023
a31d7eb
Ensure that we're validating our date range
rylnd Jun 13, 2023
370540b
Merge branch 'main' into risk_score_api
rylnd Jun 13, 2023
95895f6
Return a 404 if specified dataview is not found
rylnd Jun 14, 2023
8b3f52b
Merge branch 'main' into risk_score_api
rylnd Jun 15, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/kbn-securitysolution-io-ts-types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export * from './src/non_empty_array';
export * from './src/non_empty_or_nullable_string_array';
export * from './src/non_empty_string_array';
export * from './src/non_empty_string';
export * from './src/number_between_zero_and_one_inclusive';
export * from './src/only_false_allowed';
export * from './src/operator';
export * from './src/positive_integer_greater_than_zero';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { pipe } from 'fp-ts/lib/pipeable';
import { left } from 'fp-ts/lib/Either';
import { NumberBetweenZeroAndOneInclusive } from '.';
import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils';

describe('NumberBetweenZeroAndOneInclusive', () => {
test('it should validate 1', () => {
const payload = 1;
const decoded = NumberBetweenZeroAndOneInclusive.decode(payload);
const message = pipe(decoded, foldLeftRight);

expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});

test('it should validate a zero', () => {
const payload = 0;
const decoded = NumberBetweenZeroAndOneInclusive.decode(payload);
const message = pipe(decoded, foldLeftRight);

expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});

test('it should validate a float between 0 and 1', () => {
const payload = 0.58;
const decoded = NumberBetweenZeroAndOneInclusive.decode(payload);
const message = pipe(decoded, foldLeftRight);

expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});

test('it should NOT validate a negative number', () => {
const payload = -1;
const decoded = NumberBetweenZeroAndOneInclusive.decode(payload);
const message = pipe(decoded, foldLeftRight);

expect(getPaths(left(message.errors))).toEqual([
'Invalid value "-1" supplied to "NumberBetweenZeroAndOneInclusive"',
]);
expect(message.schema).toEqual({});
});

test('it should NOT validate NaN', () => {
const payload = NaN;
const decoded = NumberBetweenZeroAndOneInclusive.decode(payload);
const message = pipe(decoded, foldLeftRight);

expect(getPaths(left(message.errors))).toEqual([
'Invalid value "NaN" supplied to "NumberBetweenZeroAndOneInclusive"',
]);
expect(message.schema).toEqual({});
});

test('it should NOT validate Infinity', () => {
const payload = Infinity;
const decoded = NumberBetweenZeroAndOneInclusive.decode(payload);
const message = pipe(decoded, foldLeftRight);

expect(getPaths(left(message.errors))).toEqual([
'Invalid value "Infinity" supplied to "NumberBetweenZeroAndOneInclusive"',
]);
expect(message.schema).toEqual({});
});

test('it should NOT validate a string', () => {
const payload = 'some string';
const decoded = NumberBetweenZeroAndOneInclusive.decode(payload);
const message = pipe(decoded, foldLeftRight);

expect(getPaths(left(message.errors))).toEqual([
'Invalid value "some string" supplied to "NumberBetweenZeroAndOneInclusive"',
]);
expect(message.schema).toEqual({});
});
});
Original file line number Diff line number Diff line change
@@ -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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import * as t from 'io-ts';
import { Either } from 'fp-ts/lib/Either';

/**
* Types a number between 0 and 1 inclusive. Useful for specifying a probability, weighting, etc.
*/
export const NumberBetweenZeroAndOneInclusive = new t.Type<number, number, unknown>(
'NumberBetweenZeroAndOneInclusive',
t.number.is,
(input, context): Either<t.Errors, number> => {
return typeof input === 'number' &&
!Number.isNaN(input) &&
Number.isFinite(input) &&
input >= 0 &&
input <= 1
? t.success(input)
: t.failure(input, context);
},
t.identity
);
3 changes: 3 additions & 0 deletions x-pack/plugins/security_solution/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export const DEFAULT_SIGNALS_INDEX = '.siem-signals' as const;
export const DEFAULT_PREVIEW_INDEX = '.preview.alerts-security.alerts' as const;
export const DEFAULT_LISTS_INDEX = '.lists' as const;
export const DEFAULT_ITEMS_INDEX = '.items' as const;
export const DEFAULT_RISK_SCORE_PAGE_SIZE = 1000 as const;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not quite sure of a reasonable default here. This corresponds to the size of the composite agg response, so:

# identities / page_size = # of requests the Risk Score task will need to invoke

But the number of unique identities in a system is going to be quite variable, and I'm bad at guesstimates. Help?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional context: this was previously written as a terms agg, with a size of 65k terms. We decided against that route as it would limit the total number of identities that we could collect, but now we've got to decide on how to iterate through an undefined number of identities.

// The DEFAULT_MAX_SIGNALS value exists also in `x-pack/plugins/cases/common/constants.ts`
// If either changes, engineer should ensure both values are updated
export const DEFAULT_MAX_SIGNALS = 100 as const;
Expand Down Expand Up @@ -314,6 +315,8 @@ export const RISK_SCORE_CREATE_INDEX = `${INTERNAL_RISK_SCORE_URL}/indices/creat
export const RISK_SCORE_DELETE_INDICES = `${INTERNAL_RISK_SCORE_URL}/indices/delete`;
export const RISK_SCORE_CREATE_STORED_SCRIPT = `${INTERNAL_RISK_SCORE_URL}/stored_scripts/create`;
export const RISK_SCORE_DELETE_STORED_SCRIPT = `${INTERNAL_RISK_SCORE_URL}/stored_scripts/delete`;
export const RISK_SCORE_PREVIEW_URL = `${INTERNAL_RISK_SCORE_URL}/preview`;

/**
* Internal detection engine routes
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,11 @@ export const allowedExperimentalValues = Object.freeze({
* The flag doesn't have to be documented and has to be removed after the feature is ready to release.
*/
detectionsCoverageOverview: false,

/**
* Enables experimental Entity Analytics HTTP endpoints
*/
riskScoringRoutesEnabled: false,
});

type ExperimentalConfigKeys = Array<keyof ExperimentalFeatures>;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { pipe } from 'fp-ts/lib/pipeable';
import { left } from 'fp-ts/lib/Either';
import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils';

import { afterKeysSchema } from './after_keys';

describe('after_keys schema', () => {
it('allows an empty object', () => {
const payload = {};
const decoded = afterKeysSchema.decode(payload);
const message = pipe(decoded, foldLeftRight);

expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});

it('allows a valid host key', () => {
const payload = { host: { 'host.name': 'hello' } };
const decoded = afterKeysSchema.decode(payload);
const message = pipe(decoded, foldLeftRight);

expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});

it('allows a valid user key', () => {
const payload = { user: { 'user.name': 'hello' } };
const decoded = afterKeysSchema.decode(payload);
const message = pipe(decoded, foldLeftRight);

expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});

it('allows both valid host and user keys', () => {
const payload = { user: { 'user.name': 'hello' }, host: { 'host.name': 'hello' } };
const decoded = afterKeysSchema.decode(payload);
const message = pipe(decoded, foldLeftRight);

expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual(payload);
});

it('removes an unknown identifier key if used', () => {
const payload = { bad: 'key' };
const decoded = afterKeysSchema.decode(payload);
const message = pipe(decoded, foldLeftRight);

expect(getPaths(left(message.errors))).toEqual([]);
expect(message.schema).toEqual({});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import * as t from 'io-ts';

const afterKeySchema = t.record(t.string, t.string);
export type AfterKeySchema = t.TypeOf<typeof afterKeySchema>;
export type AfterKey = AfterKeySchema;

export const afterKeysSchema = t.exact(
t.partial({
host: afterKeySchema,
user: afterKeySchema,
})
);
export type AfterKeysSchema = t.TypeOf<typeof afterKeysSchema>;
export type AfterKeys = AfterKeysSchema;
Original file line number Diff line number Diff line change
@@ -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.
*/

import * as t from 'io-ts';

export const identifierTypeSchema = t.keyof({ user: null, host: null });
export type IdentifierTypeSchema = t.TypeOf<typeof identifierTypeSchema>;
export type IdentifierType = IdentifierTypeSchema;
10 changes: 10 additions & 0 deletions x-pack/plugins/security_solution/common/risk_engine/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

export * from './after_keys';
export * from './risk_weights';
export * from './identifier_types';
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import * as t from 'io-ts';
import { DataViewId } from '../../detection_engine/rule_schema';
import { afterKeysSchema } from '../after_keys';
import { identifierTypeSchema } from '../identifier_types';
import { riskWeightsSchema } from '../risk_weights/schema';

export const riskScorePreviewRequestSchema = t.exact(
t.partial({
after_keys: afterKeysSchema,
data_view_id: DataViewId,
debug: t.boolean,
filter: t.unknown,
page_size: t.number,
identifier_type: identifierTypeSchema,
range: t.type({
start: t.string,
end: t.string,
}),
weights: riskWeightsSchema,
})
);
export type RiskScorePreviewRequestSchema = t.TypeOf<typeof riskScorePreviewRequestSchema>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

export * from './types';
export type { RiskWeight, RiskWeights, GlobalRiskWeight, RiskCategoryRiskWeight } from './schema';
Loading