Skip to content

Commit e04438a

Browse files
[Security Solutions][Detection Engine] Adds threat matching API and rule type (#77395) (#77978)
## Summary This is the backend, first iteration of threat matching API and rule type. You see elements using the backend API on the front end but cannot use the UI to add or edit a threshold rule with this PR. Screen shots of it running in the UI elements that do work: <img width="1862" alt="Screen Shot 2020-09-16 at 10 34 26 AM" src="https://user-images.githubusercontent.com/1151048/93366465-6e2b9c00-f808-11ea-923b-78e8d0fdfbaa.png"> <img width="1863" alt="Screen Shot 2020-09-16 at 10 34 48 AM" src="https://user-images.githubusercontent.com/1151048/93366476-71268c80-f808-11ea-8247-d2091ff1599a.png"> **Usage** Since this is only backend API work and does not have the front end add/edit at the moment, you can use the existing UI's (for the most part) to validate the work here through CURL scripts below: Go to the folder: ```ts /kibana/x-pack/plugins/security_solution/server/lib/detection_engine/scripts ``` And post a small ECS threat mapping to the index called `mock-threat-list`: ```ts ./create_threat_mapping.sh ``` Then to post a small number of threats that represent simple port numbers you can run: ```ts ./create_threat_data.sh ``` However, feel free to also manually create them directly in your dev tools like so: ```ts # Posts a threat list item called some-name with an IP but change these out for valid data in your system PUT mock-threat-list-1/_doc/9999 { "@timestamp": "2020-09-09T20:30:45.725Z", "host": { "name": "some-name", "ip": "127.0.0.1" } } ``` ```ts # Posts a destination port number to watch PUT mock-threat-list-1/_doc/10000 { "@timestamp": "2020-09-08T20:30:45.725Z", "destination": { "port": "443" } } ``` ```ts # Posts a source port number to watch PUT mock-threat-list-1/_doc/10001 { "@timestamp": "2020-09-08T20:30:45.725Z", "source": { "port": "443" } } ``` Then you can post a threat match rule: ```ts ./post_rule.sh ./rules/queries/query_with_threat_mapping.json ``` <details> <summary>Click here to see Response</summary> ```ts { "actions": [], "author": [], "created_at": "2020-09-16T04:25:58.041Z", "created_by": "yo", "description": "Query with a threat mapping", "enabled": true, "exceptions_list": [], "false_positives": [], "from": "now-6m", "id": "f4226ab0-6f88-49c3-8f09-84cf5946ee7a", "immutable": false, "interval": "5m", "language": "kuery", "max_signals": 100, "name": "Query with a threat mapping", "output_index": ".siem-signals-hassanabad3-default", "query": "*:*", "references": [], "risk_score": 1, "risk_score_mapping": [], "rule_id": "threat-mapping", "severity": "high", "severity_mapping": [], "tags": [ "tag_1", "tag_2" ], "threat": [], "threat_index": "mock-threat-list-1", "threat_mapping": [ { "entries": [ { "field": "host.name", "type": "mapping", "value": "host.name" }, { "field": "host.ip", "type": "mapping", "value": "host.ip" } ] }, { "entries": [ { "field": "destination.ip", "type": "mapping", "value": "destination.ip" }, { "field": "destination.port", "type": "mapping", "value": "destination.port" } ] }, { "entries": [ { "field": "source.port", "type": "mapping", "value": "source.port" } ] }, { "entries": [ { "field": "source.ip", "type": "mapping", "value": "source.ip" } ] } ], "threat_query": "*:*", "throttle": "no_actions", "to": "now", "type": "threat_match", "updated_at": "2020-09-16T04:25:58.051Z", "updated_by": "yo", "version": 1 } ``` </details> **Structure** You can see the rule structure in the file: ```ts x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/query_with_threat_mapping.json ``` <details> <summary>Click here to see JSON</summary> ```ts { "name": "Query with a threat mapping", "description": "Query with a threat mapping", "rule_id": "threat-mapping", "risk_score": 1, "severity": "high", "type": "threat_match", "query": "*:*", "tags": ["tag_1", "tag_2"], "threat_index": "mock-threat-list", "threat_query": "*:*", "threat_mapping": [ { "entries": [ { "field": "host.name", "type": "mapping", "value": "host.name" }, { "field": "host.ip", "type": "mapping", "value": "host.ip" } ] }, { "entries": [ { "field": "destination.ip", "type": "mapping", "value": "destination.ip" }, { "field": "destination.port", "type": "mapping", "value": "destination.port" } ] }, { "entries": [ { "field": "source.port", "type": "mapping", "value": "source.port" } ] }, { "entries": [ { "field": "source.ip", "type": "mapping", "value": "source.ip" } ] } ] } ``` </details> Structural elements that are new: New type enum called "threat_match" ```ts "type": "threat_match", ``` New `threat_index` string which can be set to a single threat index (This might change to an array in the near future before release): ```ts "threat_index": "mock-threat-list" ``` New `threat_query` string which can be set any valid query to filter the threat list before executing the rule. This can be undefined, if you are only pushing in filters from the API. ```ts "threat_query": "*:*", ``` New `threat_filters` array which can be set to any valid filter like `filters`. This can be `undefined` if you are only using the query from the API. ```ts threat_filter": [] ``` New `threat_mapping` array which can be set to a valid mapping between the threat list and the ECS list. This structure has an inner array called `entries` which represent a 2 level tree of 1st level OR elements followed by 2nd level AND elements. For example, if you want to find all threat matches where ECS documents will match against some ${threatList} index where it would be like so: <details> <summary>Click here to see array from the boolean</summary> ```ts "threat_mapping": [ { "entries": [ { "field": "host.name", "type": "mapping", "value": "host.name" }, { "field": "host.ip", "type": "mapping", "value": "host.ip" } ] }, { "entries": [ { "field": "destination.ip", "type": "mapping", "value": "destination.ip" }, { "field": "destination.port", "type": "mapping", "value": "destination.port" } ] }, { "entries": [ { "field": "source.port", "type": "mapping", "value": "source.port" } ] }, { "entries": [ { "field": "source.ip", "type": "mapping", "value": "source.ip" } ] } ] ``` </details> What that array represents in pseudo boolean logic is: <details> <summary>Click here to see pseduo logic</summary> ```ts (host.name: ${threatList.host.name} AND host.ip: ${threatList.host.name}) OR (destination.ip: ${threatList.destination.ip} AND destination.port: ${threatList.destination.port}) OR (source.port ${threatList.source.port}) OR (source.ip ${threatList.source.ip}) ``` </details> ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios
1 parent 8b11b8f commit e04438a

File tree

56 files changed

+2719
-13
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+2719
-13
lines changed

x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,7 @@ export const type = t.keyof({
299299
query: null,
300300
saved_query: null,
301301
threshold: null,
302+
threat_match: null,
302303
});
303304
export type Type = t.TypeOf<typeof type>;
304305

x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.mock.ts

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,104 @@ export const getAddPrepackagedRulesSchemaDecodedMock = (): AddPrepackagedRulesSc
4848
exceptions_list: [],
4949
rule_id: 'rule-1',
5050
});
51+
52+
export const getAddPrepackagedThreatMatchRulesSchemaMock = (): AddPrepackagedRulesSchema => ({
53+
description: 'some description',
54+
name: 'Query with a rule id',
55+
query: 'user.name: root or user.name: admin',
56+
severity: 'high',
57+
type: 'threat_match',
58+
risk_score: 55,
59+
language: 'kuery',
60+
rule_id: 'rule-1',
61+
version: 1,
62+
threat_query: '*:*',
63+
threat_index: 'list-index',
64+
threat_mapping: [
65+
{
66+
entries: [
67+
{
68+
field: 'host.name',
69+
value: 'host.name',
70+
type: 'mapping',
71+
},
72+
],
73+
},
74+
],
75+
threat_filters: [
76+
{
77+
bool: {
78+
must: [
79+
{
80+
query_string: {
81+
query: 'host.name: linux',
82+
analyze_wildcard: true,
83+
time_zone: 'Zulu',
84+
},
85+
},
86+
],
87+
filter: [],
88+
should: [],
89+
must_not: [],
90+
},
91+
},
92+
],
93+
});
94+
95+
export const getAddPrepackagedThreatMatchRulesSchemaDecodedMock = (): AddPrepackagedRulesSchemaDecoded => ({
96+
author: [],
97+
description: 'some description',
98+
name: 'Query with a rule id',
99+
query: 'user.name: root or user.name: admin',
100+
severity: 'high',
101+
severity_mapping: [],
102+
type: 'threat_match',
103+
risk_score: 55,
104+
risk_score_mapping: [],
105+
language: 'kuery',
106+
references: [],
107+
actions: [],
108+
enabled: false,
109+
false_positives: [],
110+
from: 'now-6m',
111+
interval: '5m',
112+
max_signals: DEFAULT_MAX_SIGNALS,
113+
tags: [],
114+
to: 'now',
115+
threat: [],
116+
throttle: null,
117+
version: 1,
118+
exceptions_list: [],
119+
rule_id: 'rule-1',
120+
threat_query: '*:*',
121+
threat_index: 'list-index',
122+
threat_mapping: [
123+
{
124+
entries: [
125+
{
126+
field: 'host.name',
127+
value: 'host.name',
128+
type: 'mapping',
129+
},
130+
],
131+
},
132+
],
133+
threat_filters: [
134+
{
135+
bool: {
136+
must: [
137+
{
138+
query_string: {
139+
query: 'host.name: linux',
140+
analyze_wildcard: true,
141+
time_zone: 'Zulu',
142+
},
143+
},
144+
],
145+
filter: [],
146+
should: [],
147+
must_not: [],
148+
},
149+
},
150+
],
151+
});

x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,12 @@ import {
4545
RiskScoreMapping,
4646
SeverityMapping,
4747
} from '../common/schemas';
48+
import {
49+
threat_index,
50+
threat_query,
51+
threat_filters,
52+
threat_mapping,
53+
} from '../types/threat_mapping';
4854

4955
import {
5056
DefaultStringArray,
@@ -116,6 +122,10 @@ export const addPrepackagedRulesSchema = t.intersection([
116122
references: DefaultStringArray, // defaults to empty array of strings if not set during decode
117123
note, // defaults to "undefined" if not set during decode
118124
exceptions_list: DefaultListArray, // defaults to empty array if not set during decode
125+
threat_filters, // defaults to "undefined" if not set during decode
126+
threat_mapping, // defaults to "undefined" if not set during decode
127+
threat_query, // defaults to "undefined" if not set during decode
128+
threat_index, // defaults to "undefined" if not set during decode
119129
})
120130
),
121131
]);

x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackged_rules_schema.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import { left } from 'fp-ts/lib/Either';
1717
import {
1818
getAddPrepackagedRulesSchemaMock,
1919
getAddPrepackagedRulesSchemaDecodedMock,
20+
getAddPrepackagedThreatMatchRulesSchemaMock,
21+
getAddPrepackagedThreatMatchRulesSchemaDecodedMock,
2022
} from './add_prepackaged_rules_schema.mock';
2123
import { DEFAULT_MAX_SIGNALS } from '../../../constants';
2224
import { getListArrayMock } from '../types/lists.mock';
@@ -1597,4 +1599,16 @@ describe('add prepackaged rules schema', () => {
15971599
expect(message.schema).toEqual(expected);
15981600
});
15991601
});
1602+
1603+
describe('threat_mapping', () => {
1604+
test('You can set a threat query, index, mapping, filters on a pre-packaged rule', () => {
1605+
const payload = getAddPrepackagedThreatMatchRulesSchemaMock();
1606+
const decoded = addPrepackagedRulesSchema.decode(payload);
1607+
const checked = exactCheck(payload, decoded);
1608+
const message = pipe(checked, foldLeftRight);
1609+
const expected = getAddPrepackagedThreatMatchRulesSchemaDecodedMock();
1610+
expect(getPaths(left(message.errors))).toEqual([]);
1611+
expect(message.schema).toEqual(expected);
1612+
});
1613+
});
16001614
});

x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.mock.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,103 @@ export const getCreateRulesSchemaDecodedMock = (): CreateRulesSchemaDecoded => (
5555
exceptions_list: [],
5656
rule_id: 'rule-1',
5757
});
58+
59+
export const getCreateThreatMatchRulesSchemaMock = (ruleId = 'rule-1'): CreateRulesSchema => ({
60+
description: 'Detecting root and admin users',
61+
name: 'Query with a rule id',
62+
query: 'user.name: root or user.name: admin',
63+
severity: 'high',
64+
type: 'threat_match',
65+
risk_score: 55,
66+
language: 'kuery',
67+
rule_id: ruleId,
68+
threat_query: '*:*',
69+
threat_index: 'list-index',
70+
threat_mapping: [
71+
{
72+
entries: [
73+
{
74+
field: 'host.name',
75+
value: 'host.name',
76+
type: 'mapping',
77+
},
78+
],
79+
},
80+
],
81+
threat_filters: [
82+
{
83+
bool: {
84+
must: [
85+
{
86+
query_string: {
87+
query: 'host.name: linux',
88+
analyze_wildcard: true,
89+
time_zone: 'Zulu',
90+
},
91+
},
92+
],
93+
filter: [],
94+
should: [],
95+
must_not: [],
96+
},
97+
},
98+
],
99+
});
100+
101+
export const getCreateThreatMatchRulesSchemaDecodedMock = (): CreateRulesSchemaDecoded => ({
102+
author: [],
103+
severity_mapping: [],
104+
risk_score_mapping: [],
105+
description: 'Detecting root and admin users',
106+
name: 'Query with a rule id',
107+
query: 'user.name: root or user.name: admin',
108+
severity: 'high',
109+
type: 'threat_match',
110+
risk_score: 55,
111+
language: 'kuery',
112+
references: [],
113+
actions: [],
114+
enabled: true,
115+
false_positives: [],
116+
from: 'now-6m',
117+
interval: '5m',
118+
max_signals: DEFAULT_MAX_SIGNALS,
119+
tags: [],
120+
to: 'now',
121+
threat: [],
122+
throttle: null,
123+
version: 1,
124+
exceptions_list: [],
125+
rule_id: 'rule-1',
126+
threat_query: '*:*',
127+
threat_index: 'list-index',
128+
threat_mapping: [
129+
{
130+
entries: [
131+
{
132+
field: 'host.name',
133+
value: 'host.name',
134+
type: 'mapping',
135+
},
136+
],
137+
},
138+
],
139+
threat_filters: [
140+
{
141+
bool: {
142+
must: [
143+
{
144+
query_string: {
145+
query: 'host.name: linux',
146+
analyze_wildcard: true,
147+
time_zone: 'Zulu',
148+
},
149+
},
150+
],
151+
filter: [],
152+
should: [],
153+
must_not: [],
154+
},
155+
},
156+
],
157+
});

x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import { left } from 'fp-ts/lib/Either';
1616
import {
1717
getCreateRulesSchemaMock,
1818
getCreateRulesSchemaDecodedMock,
19+
getCreateThreatMatchRulesSchemaMock,
20+
getCreateThreatMatchRulesSchemaDecodedMock,
1921
} from './create_rules_schema.mock';
2022
import { DEFAULT_MAX_SIGNALS } from '../../../constants';
2123
import { getListArrayMock } from '../types/lists.mock';
@@ -1661,4 +1663,16 @@ describe('create rules schema', () => {
16611663
expect(message.schema).toEqual(expected);
16621664
});
16631665
});
1666+
1667+
describe('threat_mapping', () => {
1668+
test('You can set a threat query, index, mapping, filters when creating a rule', () => {
1669+
const payload = getCreateThreatMatchRulesSchemaMock();
1670+
const decoded = createRulesSchema.decode(payload);
1671+
const checked = exactCheck(payload, decoded);
1672+
const message = pipe(checked, foldLeftRight);
1673+
const expected = getCreateThreatMatchRulesSchemaDecodedMock();
1674+
expect(getPaths(left(message.errors))).toEqual([]);
1675+
expect(message.schema).toEqual(expected);
1676+
});
1677+
});
16641678
});

x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,12 @@ import {
4646
RiskScoreMapping,
4747
SeverityMapping,
4848
} from '../common/schemas';
49+
import {
50+
threat_index,
51+
threat_query,
52+
threat_filters,
53+
threat_mapping,
54+
} from '../types/threat_mapping';
4955

5056
import {
5157
DefaultStringArray,
@@ -112,6 +118,10 @@ export const createRulesSchema = t.intersection([
112118
note, // defaults to "undefined" if not set during decode
113119
version: DefaultVersionNumber, // defaults to 1 if not set during decode
114120
exceptions_list: DefaultListArray, // defaults to empty array if not set during decode
121+
threat_mapping, // defaults to "undefined" if not set during decode
122+
threat_query, // defaults to "undefined" if not set during decode
123+
threat_filters, // defaults to "undefined" if not set during decode
124+
threat_index, // defaults to "undefined" if not set during decode
115125
})
116126
),
117127
]);

x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_type_dependents.test.ts

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44
* you may not use this file except in compliance with the Elastic License.
55
*/
66

7-
import { getCreateRulesSchemaMock } from './create_rules_schema.mock';
7+
import {
8+
getCreateRulesSchemaMock,
9+
getCreateThreatMatchRulesSchemaMock,
10+
} from './create_rules_schema.mock';
811
import { CreateRulesSchema } from './create_rules_schema';
912
import { createRuleValidateTypeDependents } from './create_rules_type_dependents';
1013

@@ -87,4 +90,39 @@ describe('create_rules_type_dependents', () => {
8790
const errors = createRuleValidateTypeDependents(schema);
8891
expect(errors).toEqual(['"threshold.value" has to be bigger than 0']);
8992
});
93+
94+
test('threat_index, threat_query, and threat_mapping are required when type is "threat_match" and validates with it', () => {
95+
const schema: CreateRulesSchema = {
96+
...getCreateRulesSchemaMock(),
97+
type: 'threat_match',
98+
};
99+
const errors = createRuleValidateTypeDependents(schema);
100+
expect(errors).toEqual([
101+
'when "type" is "threat_match", "threat_index" is required',
102+
'when "type" is "threat_match", "threat_query" is required',
103+
'when "type" is "threat_match", "threat_mapping" is required',
104+
]);
105+
});
106+
107+
test('validates with threat_index, threat_query, and threat_mapping when type is "threat_match"', () => {
108+
const schema = getCreateThreatMatchRulesSchemaMock();
109+
const { threat_filters: threatFilters, ...noThreatFilters } = schema;
110+
const errors = createRuleValidateTypeDependents(noThreatFilters);
111+
expect(errors).toEqual([]);
112+
});
113+
114+
test('does NOT validate when threat_mapping is an empty array', () => {
115+
const schema: CreateRulesSchema = {
116+
...getCreateThreatMatchRulesSchemaMock(),
117+
threat_mapping: [],
118+
};
119+
const errors = createRuleValidateTypeDependents(schema);
120+
expect(errors).toEqual(['threat_mapping" must have at least one element']);
121+
});
122+
123+
test('validates with threat_index, threat_query, threat_mapping, and an optional threat_filters, when type is "threat_match"', () => {
124+
const schema = getCreateThreatMatchRulesSchemaMock();
125+
const errors = createRuleValidateTypeDependents(schema);
126+
expect(errors).toEqual([]);
127+
});
90128
});

0 commit comments

Comments
 (0)