Skip to content

Commit 73c5a27

Browse files
[Security Solution][RAC] - Update reason field text (#110308) (#111020)
Co-authored-by: Michael Olorunnisola <michael.olorunnisola@elastic.co>
1 parent b3227b7 commit 73c5a27

File tree

6 files changed

+226
-49
lines changed

6 files changed

+226
-49
lines changed

x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,10 +121,9 @@ export const buildSignalFromSequence = (
121121
): SignalHit => {
122122
const rule = buildRuleWithoutOverrides(ruleSO);
123123
const timestamp = new Date().toISOString();
124-
125-
const reason = buildReasonMessage({ rule });
126-
const signal: Signal = buildSignal(events, rule, reason);
127124
const mergedEvents = objectArrayIntersection(events.map((event) => event._source));
125+
const reason = buildReasonMessage({ rule, mergedDoc: mergedEvents as SignalSourceHit });
126+
const signal: Signal = buildSignal(events, rule, reason);
128127
return {
129128
...mergedEvents,
130129
'@timestamp': timestamp,

x-pack/plugins/security_solution/server/lib/detection_engine/signals/reason_formatter.test.ts

Lines changed: 124 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* 2.0.
66
*/
77

8-
import { buildCommonReasonMessage } from './reason_formatters';
8+
import { buildReasonMessageUtil } from './reason_formatters';
99
import { RulesSchema } from '../../../../common/detection_engine/schemas/response/rules_schema';
1010
import { SignalSourceHit } from './types';
1111

@@ -14,26 +14,48 @@ describe('reason_formatter', () => {
1414
let mergedDoc: SignalSourceHit;
1515
beforeAll(() => {
1616
rule = {
17-
name: 'What is in a name',
17+
name: 'my-rule',
1818
risk_score: 9000,
1919
severity: 'medium',
2020
} as RulesSchema; // Cast here as all fields aren't required
2121
mergedDoc = {
22-
_index: 'some-index',
23-
_id: 'some-id',
22+
_index: 'index-1',
23+
_id: 'id-1',
2424
fields: {
25-
'host.name': ['party host'],
26-
'user.name': ['ferris bueller'],
25+
'destination.address': ['9.99.99.9'],
26+
'destination.port': ['6789'],
27+
'event.category': ['test'],
28+
'file.name': ['sample'],
29+
'host.name': ['host'],
30+
'process.name': ['doingThings.exe'],
31+
'process.parent.name': ['didThings.exe'],
32+
'source.address': ['1.11.11.1'],
33+
'source.port': ['1234'],
34+
'user.name': ['test-user'],
2735
'@timestamp': '2021-08-11T02:28:59.101Z',
2836
},
2937
};
3038
});
3139

32-
describe('buildCommonReasonMessage', () => {
40+
describe('buildReasonMessageUtil', () => {
3341
describe('when rule and mergedDoc are provided', () => {
3442
it('should return the full reason message', () => {
35-
expect(buildCommonReasonMessage({ rule, mergedDoc })).toEqual(
36-
'Alert What is in a name created with a medium severity and risk score of 9000 by ferris bueller on party host.'
43+
expect(buildReasonMessageUtil({ rule, mergedDoc })).toMatchInlineSnapshot(
44+
`"test event with process doingThings.exe, parent process didThings.exe, file sample, source 1.11.11.1:1234, destination 9.99.99.9:6789, by test-user on host created medium alert my-rule."`
45+
);
46+
});
47+
});
48+
describe('when event category contains multiple items', () => {
49+
it('should return the reason message with all categories showing', () => {
50+
const updatedMergedDoc = {
51+
...mergedDoc,
52+
fields: {
53+
...mergedDoc.fields,
54+
'event.category': ['item one', 'item two'],
55+
},
56+
};
57+
expect(buildReasonMessageUtil({ rule, mergedDoc: updatedMergedDoc })).toMatchInlineSnapshot(
58+
`"item one, item two event with process doingThings.exe, parent process didThings.exe, file sample, source 1.11.11.1:1234, destination 9.99.99.9:6789, by test-user on host created medium alert my-rule."`
3759
);
3860
});
3961
});
@@ -46,8 +68,8 @@ describe('reason_formatter', () => {
4668
'host.name': ['-'],
4769
},
4870
};
49-
expect(buildCommonReasonMessage({ rule, mergedDoc: updatedMergedDoc })).toEqual(
50-
'Alert What is in a name created with a medium severity and risk score of 9000 by ferris bueller.'
71+
expect(buildReasonMessageUtil({ rule, mergedDoc: updatedMergedDoc })).toMatchInlineSnapshot(
72+
`"test event with process doingThings.exe, parent process didThings.exe, file sample, source 1.11.11.1:1234, destination 9.99.99.9:6789, by test-user created medium alert my-rule."`
5173
);
5274
});
5375
});
@@ -60,16 +82,102 @@ describe('reason_formatter', () => {
6082
'user.name': ['-'],
6183
},
6284
};
63-
expect(buildCommonReasonMessage({ rule, mergedDoc: updatedMergedDoc })).toEqual(
64-
'Alert What is in a name created with a medium severity and risk score of 9000 on party host.'
85+
expect(buildReasonMessageUtil({ rule, mergedDoc: updatedMergedDoc })).toMatchInlineSnapshot(
86+
`"test event with process doingThings.exe, parent process didThings.exe, file sample, source 1.11.11.1:1234, destination 9.99.99.9:6789, on host created medium alert my-rule."`
87+
);
88+
});
89+
});
90+
describe('when rule and mergedDoc are provided, but destination details are missing', () => {
91+
it('should return the reason message without the destination port', () => {
92+
const noDestinationPortDoc = {
93+
...mergedDoc,
94+
fields: {
95+
...mergedDoc.fields,
96+
'destination.port': ['-'],
97+
},
98+
};
99+
expect(
100+
buildReasonMessageUtil({ rule, mergedDoc: noDestinationPortDoc })
101+
).toMatchInlineSnapshot(
102+
`"test event with process doingThings.exe, parent process didThings.exe, file sample, source 1.11.11.1:1234, destination 9.99.99.9 by test-user on host created medium alert my-rule."`
103+
);
104+
});
105+
it('should return the reason message without destination details', () => {
106+
const noDestinationPortDoc = {
107+
...mergedDoc,
108+
fields: {
109+
...mergedDoc.fields,
110+
'destination.address': ['-'],
111+
'destination.port': ['-'],
112+
},
113+
};
114+
expect(
115+
buildReasonMessageUtil({ rule, mergedDoc: noDestinationPortDoc })
116+
).toMatchInlineSnapshot(
117+
`"test event with process doingThings.exe, parent process didThings.exe, file sample, source 1.11.11.1:1234, by test-user on host created medium alert my-rule."`
118+
);
119+
});
120+
});
121+
describe('when rule and mergedDoc are provided, but source details are missing', () => {
122+
it('should return the reason message without the source port', () => {
123+
const noSourcePortDoc = {
124+
...mergedDoc,
125+
fields: {
126+
...mergedDoc.fields,
127+
'source.port': ['-'],
128+
},
129+
};
130+
expect(buildReasonMessageUtil({ rule, mergedDoc: noSourcePortDoc })).toMatchInlineSnapshot(
131+
`"test event with process doingThings.exe, parent process didThings.exe, file sample, source 1.11.11.1 destination 9.99.99.9:6789, by test-user on host created medium alert my-rule."`
132+
);
133+
});
134+
it('should return the reason message without source details', () => {
135+
const noSourcePortDoc = {
136+
...mergedDoc,
137+
fields: {
138+
...mergedDoc.fields,
139+
'source.address': ['-'],
140+
'source.port': ['-'],
141+
},
142+
};
143+
expect(buildReasonMessageUtil({ rule, mergedDoc: noSourcePortDoc })).toMatchInlineSnapshot(
144+
`"test event with process doingThings.exe, parent process didThings.exe, file sample, destination 9.99.99.9:6789, by test-user on host created medium alert my-rule."`
145+
);
146+
});
147+
});
148+
describe('when rule and mergedDoc are provided, but process details missing', () => {
149+
it('should return the reason message without process details', () => {
150+
const updatedMergedDoc = {
151+
...mergedDoc,
152+
fields: {
153+
...mergedDoc.fields,
154+
'process.name': ['-'],
155+
'process.parent.name': ['-'],
156+
},
157+
};
158+
expect(buildReasonMessageUtil({ rule, mergedDoc: updatedMergedDoc })).toMatchInlineSnapshot(
159+
`"test event with file sample, source 1.11.11.1:1234, destination 9.99.99.9:6789, by test-user on host created medium alert my-rule."`
160+
);
161+
});
162+
});
163+
describe('when rule and mergedDoc are provided without any fields of interest', () => {
164+
it('should return the full reason message', () => {
165+
const updatedMergedDoc = {
166+
...mergedDoc,
167+
fields: {
168+
'event.category': ['test'],
169+
'user.name': ['test-user'],
170+
'@timestamp': '2021-08-11T02:28:59.101Z',
171+
},
172+
};
173+
expect(buildReasonMessageUtil({ rule, mergedDoc: updatedMergedDoc })).toMatchInlineSnapshot(
174+
`"test event by test-user created medium alert my-rule."`
65175
);
66176
});
67177
});
68178
describe('when only rule is provided', () => {
69179
it('should return the reason message without host name or user name', () => {
70-
expect(buildCommonReasonMessage({ rule })).toEqual(
71-
'Alert What is in a name created with a medium severity and risk score of 9000.'
72-
);
180+
expect(buildReasonMessageUtil({ rule })).toMatchInlineSnapshot(`""`);
73181
});
74182
});
75183
});

x-pack/plugins/security_solution/server/lib/detection_engine/signals/reason_formatters.ts

Lines changed: 85 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
*/
77

88
import { i18n } from '@kbn/i18n';
9+
import { getOr } from 'lodash/fp';
910
import { RulesSchema } from '../../../../common/detection_engine/schemas/response/rules_schema';
1011
import { SignalSourceHit } from './types';
1112

@@ -14,54 +15,118 @@ export interface BuildReasonMessageArgs {
1415
mergedDoc?: SignalSourceHit;
1516
}
1617

18+
export interface BuildReasonMessageUtilArgs extends BuildReasonMessageArgs {
19+
type?: 'eql' | 'ml' | 'query' | 'threatMatch' | 'threshold';
20+
}
21+
1722
export type BuildReasonMessage = (args: BuildReasonMessageArgs) => string;
1823

24+
interface ReasonFields {
25+
destinationAddress?: string | string[] | null;
26+
destinationPort?: string | string[] | null;
27+
eventCategory?: string | string[] | null;
28+
fileName?: string | string[] | null;
29+
hostName?: string | string[] | null;
30+
processName?: string | string[] | null;
31+
processParentName?: string | string[] | null;
32+
sourceAddress?: string | string[] | null;
33+
sourcePort?: string | string[] | null;
34+
userName?: string | string[] | null;
35+
}
36+
const getFieldsFromDoc = (mergedDoc: SignalSourceHit) => {
37+
const reasonFields: ReasonFields = {};
38+
const docToUse = mergedDoc?.fields || mergedDoc;
39+
40+
reasonFields.destinationAddress = getOr(null, 'destination.address', docToUse);
41+
reasonFields.destinationPort = getOr(null, 'destination.port', docToUse);
42+
reasonFields.eventCategory = getOr(null, 'event.category', docToUse);
43+
reasonFields.fileName = getOr(null, 'file.name', docToUse);
44+
reasonFields.hostName = getOr(null, 'host.name', docToUse);
45+
reasonFields.processName = getOr(null, 'process.name', docToUse);
46+
reasonFields.processParentName = getOr(null, 'process.parent.name', docToUse);
47+
reasonFields.sourceAddress = getOr(null, 'source.address', docToUse);
48+
reasonFields.sourcePort = getOr(null, 'source.port', docToUse);
49+
reasonFields.userName = getOr(null, 'user.name', docToUse);
50+
51+
return reasonFields;
52+
};
1953
/**
2054
* Currently all security solution rule types share a common reason message string. This function composes that string
2155
* In the future there may be different configurations based on the different rule types, so the plumbing has been put in place
2256
* to more easily allow for this in the future.
2357
* @export buildCommonReasonMessage - is only exported for testing purposes, and only used internally here.
2458
*/
25-
export const buildCommonReasonMessage = ({ rule, mergedDoc }: BuildReasonMessageArgs) => {
26-
if (!rule) {
59+
export const buildReasonMessageUtil = ({ rule, mergedDoc }: BuildReasonMessageUtilArgs) => {
60+
if (!rule || !mergedDoc) {
2761
// This should never happen, but in case, better to not show a malformed string
2862
return '';
2963
}
30-
let hostName;
31-
let userName;
32-
if (mergedDoc?.fields) {
33-
hostName = mergedDoc.fields['host.name'] != null ? mergedDoc.fields['host.name'] : hostName;
34-
userName = mergedDoc.fields['user.name'] != null ? mergedDoc.fields['user.name'] : userName;
35-
}
64+
const {
65+
destinationAddress,
66+
destinationPort,
67+
eventCategory,
68+
fileName,
69+
hostName,
70+
processName,
71+
processParentName,
72+
sourceAddress,
73+
sourcePort,
74+
userName,
75+
} = getFieldsFromDoc(mergedDoc);
76+
77+
const fieldPresenceTracker = { hasFieldOfInterest: false };
3678

37-
const isFieldEmpty = (field: string | string[] | undefined | null) =>
38-
!field || !field.length || (field.length === 1 && field[0] === '-');
79+
const getFieldTemplateValue = (
80+
field: string | string[] | undefined | null,
81+
isFieldOfInterest?: boolean
82+
): string | null => {
83+
if (!field || !field.length || (field.length === 1 && field[0] === '-')) return null;
84+
if (isFieldOfInterest && !fieldPresenceTracker.hasFieldOfInterest)
85+
fieldPresenceTracker.hasFieldOfInterest = true;
86+
return Array.isArray(field) ? field.join(', ') : field;
87+
};
3988

4089
return i18n.translate('xpack.securitySolution.detectionEngine.signals.alertReasonDescription', {
41-
defaultMessage:
42-
'Alert {alertName} created with a {alertSeverity} severity and risk score of {alertRiskScore}{userName, select, null {} other {{whitespace}by {userName}} }{hostName, select, null {} other {{whitespace}on {hostName}} }.',
90+
defaultMessage: `{eventCategory, select, null {} other {{eventCategory}{whitespace}}}event\
91+
{hasFieldOfInterest, select, false {} other {{whitespace}with}}\
92+
{processName, select, null {} other {{whitespace}process {processName},} }\
93+
{processParentName, select, null {} other {{whitespace}parent process {processParentName},} }\
94+
{fileName, select, null {} other {{whitespace}file {fileName},} }\
95+
{sourceAddress, select, null {} other {{whitespace}source {sourceAddress}}}{sourcePort, select, null {} other {:{sourcePort},}}\
96+
{destinationAddress, select, null {} other {{whitespace}destination {destinationAddress}}}{destinationPort, select, null {} other {:{destinationPort},}}\
97+
{userName, select, null {} other {{whitespace}by {userName}} }\
98+
{hostName, select, null {} other {{whitespace}on {hostName}} } \
99+
created {alertSeverity} alert {alertName}.`,
43100
values: {
44101
alertName: rule.name,
45102
alertSeverity: rule.severity,
46-
alertRiskScore: rule.risk_score,
47-
hostName: isFieldEmpty(hostName) ? 'null' : hostName,
48-
userName: isFieldEmpty(userName) ? 'null' : userName,
103+
destinationAddress: getFieldTemplateValue(destinationAddress, true),
104+
destinationPort: getFieldTemplateValue(destinationPort, true),
105+
eventCategory: getFieldTemplateValue(eventCategory),
106+
fileName: getFieldTemplateValue(fileName, true),
107+
hostName: getFieldTemplateValue(hostName),
108+
processName: getFieldTemplateValue(processName, true),
109+
processParentName: getFieldTemplateValue(processParentName, true),
110+
sourceAddress: getFieldTemplateValue(sourceAddress, true),
111+
sourcePort: getFieldTemplateValue(sourcePort, true),
112+
userName: getFieldTemplateValue(userName),
113+
hasFieldOfInterest: fieldPresenceTracker.hasFieldOfInterest, // Tracking if we have any fields to show the 'with' word
49114
whitespace: ' ', // there isn't support for the unicode /u0020 for whitespace, and leading spaces are deleted, so to prevent double-whitespace explicitly passing the space in.
50115
},
51116
});
52117
};
53118

54119
export const buildReasonMessageForEqlAlert = (args: BuildReasonMessageArgs) =>
55-
buildCommonReasonMessage({ ...args });
120+
buildReasonMessageUtil({ ...args, type: 'eql' });
56121

57122
export const buildReasonMessageForMlAlert = (args: BuildReasonMessageArgs) =>
58-
buildCommonReasonMessage({ ...args });
123+
buildReasonMessageUtil({ ...args, type: 'ml' });
59124

60125
export const buildReasonMessageForQueryAlert = (args: BuildReasonMessageArgs) =>
61-
buildCommonReasonMessage({ ...args });
126+
buildReasonMessageUtil({ ...args, type: 'query' });
62127

63128
export const buildReasonMessageForThreatMatchAlert = (args: BuildReasonMessageArgs) =>
64-
buildCommonReasonMessage({ ...args });
129+
buildReasonMessageUtil({ ...args, type: 'threatMatch' });
65130

66131
export const buildReasonMessageForThresholdAlert = (args: BuildReasonMessageArgs) =>
67-
buildCommonReasonMessage({ ...args });
132+
buildReasonMessageUtil({ ...args, type: 'threshold' });

x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_ml.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ export default ({ getService }: FtrProviderContext) => {
193193
index: '.ml-anomalies-custom-linux_anomalous_network_activity_ecs',
194194
depth: 0,
195195
},
196-
reason: `Alert Test ML rule created with a critical severity and risk score of 50 by root on mothra.`,
196+
reason: `event with process store, by root on mothra created critical alert Test ML rule.`,
197197
original_time: '2020-11-16T22:58:08.000Z',
198198
},
199199
all_field_values: [

x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,8 @@ export default ({ getService }: FtrProviderContext) => {
275275
depth: 0,
276276
},
277277
],
278-
reason: `Alert Query with a rule id created with a high severity and risk score of 55 by root on zeek-sensor-amsterdam.`,
278+
reason:
279+
'user-login event by root on zeek-sensor-amsterdam created high alert Query with a rule id.',
279280
rule: fullSignal.signal.rule,
280281
status: 'open',
281282
},

0 commit comments

Comments
 (0)