[Security Solution][RAC] - Update reason field text (#110308)

This commit is contained in:
Michael Olorunnisola 2021-09-02 13:52:10 -04:00 committed by GitHub
parent f8e86f5e02
commit 3dda4dafa0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 226 additions and 49 deletions

View file

@ -121,10 +121,9 @@ export const buildSignalFromSequence = (
): SignalHit => {
const rule = buildRuleWithoutOverrides(ruleSO);
const timestamp = new Date().toISOString();
const reason = buildReasonMessage({ rule });
const signal: Signal = buildSignal(events, rule, reason);
const mergedEvents = objectArrayIntersection(events.map((event) => event._source));
const reason = buildReasonMessage({ rule, mergedDoc: mergedEvents as SignalSourceHit });
const signal: Signal = buildSignal(events, rule, reason);
return {
...mergedEvents,
'@timestamp': timestamp,

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { buildCommonReasonMessage } from './reason_formatters';
import { buildReasonMessageUtil } from './reason_formatters';
import { RulesSchema } from '../../../../common/detection_engine/schemas/response/rules_schema';
import { SignalSourceHit } from './types';
@ -14,26 +14,48 @@ describe('reason_formatter', () => {
let mergedDoc: SignalSourceHit;
beforeAll(() => {
rule = {
name: 'What is in a name',
name: 'my-rule',
risk_score: 9000,
severity: 'medium',
} as RulesSchema; // Cast here as all fields aren't required
mergedDoc = {
_index: 'some-index',
_id: 'some-id',
_index: 'index-1',
_id: 'id-1',
fields: {
'host.name': ['party host'],
'user.name': ['ferris bueller'],
'destination.address': ['9.99.99.9'],
'destination.port': ['6789'],
'event.category': ['test'],
'file.name': ['sample'],
'host.name': ['host'],
'process.name': ['doingThings.exe'],
'process.parent.name': ['didThings.exe'],
'source.address': ['1.11.11.1'],
'source.port': ['1234'],
'user.name': ['test-user'],
'@timestamp': '2021-08-11T02:28:59.101Z',
},
};
});
describe('buildCommonReasonMessage', () => {
describe('buildReasonMessageUtil', () => {
describe('when rule and mergedDoc are provided', () => {
it('should return the full reason message', () => {
expect(buildCommonReasonMessage({ rule, mergedDoc })).toEqual(
'Alert What is in a name created with a medium severity and risk score of 9000 by ferris bueller on party host.'
expect(buildReasonMessageUtil({ rule, mergedDoc })).toMatchInlineSnapshot(
`"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."`
);
});
});
describe('when event category contains multiple items', () => {
it('should return the reason message with all categories showing', () => {
const updatedMergedDoc = {
...mergedDoc,
fields: {
...mergedDoc.fields,
'event.category': ['item one', 'item two'],
},
};
expect(buildReasonMessageUtil({ rule, mergedDoc: updatedMergedDoc })).toMatchInlineSnapshot(
`"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."`
);
});
});
@ -46,8 +68,8 @@ describe('reason_formatter', () => {
'host.name': ['-'],
},
};
expect(buildCommonReasonMessage({ rule, mergedDoc: updatedMergedDoc })).toEqual(
'Alert What is in a name created with a medium severity and risk score of 9000 by ferris bueller.'
expect(buildReasonMessageUtil({ rule, mergedDoc: updatedMergedDoc })).toMatchInlineSnapshot(
`"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."`
);
});
});
@ -60,16 +82,102 @@ describe('reason_formatter', () => {
'user.name': ['-'],
},
};
expect(buildCommonReasonMessage({ rule, mergedDoc: updatedMergedDoc })).toEqual(
'Alert What is in a name created with a medium severity and risk score of 9000 on party host.'
expect(buildReasonMessageUtil({ rule, mergedDoc: updatedMergedDoc })).toMatchInlineSnapshot(
`"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."`
);
});
});
describe('when rule and mergedDoc are provided, but destination details are missing', () => {
it('should return the reason message without the destination port', () => {
const noDestinationPortDoc = {
...mergedDoc,
fields: {
...mergedDoc.fields,
'destination.port': ['-'],
},
};
expect(
buildReasonMessageUtil({ rule, mergedDoc: noDestinationPortDoc })
).toMatchInlineSnapshot(
`"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."`
);
});
it('should return the reason message without destination details', () => {
const noDestinationPortDoc = {
...mergedDoc,
fields: {
...mergedDoc.fields,
'destination.address': ['-'],
'destination.port': ['-'],
},
};
expect(
buildReasonMessageUtil({ rule, mergedDoc: noDestinationPortDoc })
).toMatchInlineSnapshot(
`"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."`
);
});
});
describe('when rule and mergedDoc are provided, but source details are missing', () => {
it('should return the reason message without the source port', () => {
const noSourcePortDoc = {
...mergedDoc,
fields: {
...mergedDoc.fields,
'source.port': ['-'],
},
};
expect(buildReasonMessageUtil({ rule, mergedDoc: noSourcePortDoc })).toMatchInlineSnapshot(
`"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."`
);
});
it('should return the reason message without source details', () => {
const noSourcePortDoc = {
...mergedDoc,
fields: {
...mergedDoc.fields,
'source.address': ['-'],
'source.port': ['-'],
},
};
expect(buildReasonMessageUtil({ rule, mergedDoc: noSourcePortDoc })).toMatchInlineSnapshot(
`"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."`
);
});
});
describe('when rule and mergedDoc are provided, but process details missing', () => {
it('should return the reason message without process details', () => {
const updatedMergedDoc = {
...mergedDoc,
fields: {
...mergedDoc.fields,
'process.name': ['-'],
'process.parent.name': ['-'],
},
};
expect(buildReasonMessageUtil({ rule, mergedDoc: updatedMergedDoc })).toMatchInlineSnapshot(
`"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."`
);
});
});
describe('when rule and mergedDoc are provided without any fields of interest', () => {
it('should return the full reason message', () => {
const updatedMergedDoc = {
...mergedDoc,
fields: {
'event.category': ['test'],
'user.name': ['test-user'],
'@timestamp': '2021-08-11T02:28:59.101Z',
},
};
expect(buildReasonMessageUtil({ rule, mergedDoc: updatedMergedDoc })).toMatchInlineSnapshot(
`"test event by test-user created medium alert my-rule."`
);
});
});
describe('when only rule is provided', () => {
it('should return the reason message without host name or user name', () => {
expect(buildCommonReasonMessage({ rule })).toEqual(
'Alert What is in a name created with a medium severity and risk score of 9000.'
);
expect(buildReasonMessageUtil({ rule })).toMatchInlineSnapshot(`""`);
});
});
});

View file

@ -6,6 +6,7 @@
*/
import { i18n } from '@kbn/i18n';
import { getOr } from 'lodash/fp';
import { RulesSchema } from '../../../../common/detection_engine/schemas/response/rules_schema';
import { SignalSourceHit } from './types';
@ -14,54 +15,118 @@ export interface BuildReasonMessageArgs {
mergedDoc?: SignalSourceHit;
}
export interface BuildReasonMessageUtilArgs extends BuildReasonMessageArgs {
type?: 'eql' | 'ml' | 'query' | 'threatMatch' | 'threshold';
}
export type BuildReasonMessage = (args: BuildReasonMessageArgs) => string;
interface ReasonFields {
destinationAddress?: string | string[] | null;
destinationPort?: string | string[] | null;
eventCategory?: string | string[] | null;
fileName?: string | string[] | null;
hostName?: string | string[] | null;
processName?: string | string[] | null;
processParentName?: string | string[] | null;
sourceAddress?: string | string[] | null;
sourcePort?: string | string[] | null;
userName?: string | string[] | null;
}
const getFieldsFromDoc = (mergedDoc: SignalSourceHit) => {
const reasonFields: ReasonFields = {};
const docToUse = mergedDoc?.fields || mergedDoc;
reasonFields.destinationAddress = getOr(null, 'destination.address', docToUse);
reasonFields.destinationPort = getOr(null, 'destination.port', docToUse);
reasonFields.eventCategory = getOr(null, 'event.category', docToUse);
reasonFields.fileName = getOr(null, 'file.name', docToUse);
reasonFields.hostName = getOr(null, 'host.name', docToUse);
reasonFields.processName = getOr(null, 'process.name', docToUse);
reasonFields.processParentName = getOr(null, 'process.parent.name', docToUse);
reasonFields.sourceAddress = getOr(null, 'source.address', docToUse);
reasonFields.sourcePort = getOr(null, 'source.port', docToUse);
reasonFields.userName = getOr(null, 'user.name', docToUse);
return reasonFields;
};
/**
* Currently all security solution rule types share a common reason message string. This function composes that string
* In the future there may be different configurations based on the different rule types, so the plumbing has been put in place
* to more easily allow for this in the future.
* @export buildCommonReasonMessage - is only exported for testing purposes, and only used internally here.
*/
export const buildCommonReasonMessage = ({ rule, mergedDoc }: BuildReasonMessageArgs) => {
if (!rule) {
export const buildReasonMessageUtil = ({ rule, mergedDoc }: BuildReasonMessageUtilArgs) => {
if (!rule || !mergedDoc) {
// This should never happen, but in case, better to not show a malformed string
return '';
}
let hostName;
let userName;
if (mergedDoc?.fields) {
hostName = mergedDoc.fields['host.name'] != null ? mergedDoc.fields['host.name'] : hostName;
userName = mergedDoc.fields['user.name'] != null ? mergedDoc.fields['user.name'] : userName;
}
const {
destinationAddress,
destinationPort,
eventCategory,
fileName,
hostName,
processName,
processParentName,
sourceAddress,
sourcePort,
userName,
} = getFieldsFromDoc(mergedDoc);
const isFieldEmpty = (field: string | string[] | undefined | null) =>
!field || !field.length || (field.length === 1 && field[0] === '-');
const fieldPresenceTracker = { hasFieldOfInterest: false };
const getFieldTemplateValue = (
field: string | string[] | undefined | null,
isFieldOfInterest?: boolean
): string | null => {
if (!field || !field.length || (field.length === 1 && field[0] === '-')) return null;
if (isFieldOfInterest && !fieldPresenceTracker.hasFieldOfInterest)
fieldPresenceTracker.hasFieldOfInterest = true;
return Array.isArray(field) ? field.join(', ') : field;
};
return i18n.translate('xpack.securitySolution.detectionEngine.signals.alertReasonDescription', {
defaultMessage:
'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}} }.',
defaultMessage: `{eventCategory, select, null {} other {{eventCategory}{whitespace}}}event\
{hasFieldOfInterest, select, false {} other {{whitespace}with}}\
{processName, select, null {} other {{whitespace}process {processName},} }\
{processParentName, select, null {} other {{whitespace}parent process {processParentName},} }\
{fileName, select, null {} other {{whitespace}file {fileName},} }\
{sourceAddress, select, null {} other {{whitespace}source {sourceAddress}}}{sourcePort, select, null {} other {:{sourcePort},}}\
{destinationAddress, select, null {} other {{whitespace}destination {destinationAddress}}}{destinationPort, select, null {} other {:{destinationPort},}}\
{userName, select, null {} other {{whitespace}by {userName}} }\
{hostName, select, null {} other {{whitespace}on {hostName}} } \
created {alertSeverity} alert {alertName}.`,
values: {
alertName: rule.name,
alertSeverity: rule.severity,
alertRiskScore: rule.risk_score,
hostName: isFieldEmpty(hostName) ? 'null' : hostName,
userName: isFieldEmpty(userName) ? 'null' : userName,
destinationAddress: getFieldTemplateValue(destinationAddress, true),
destinationPort: getFieldTemplateValue(destinationPort, true),
eventCategory: getFieldTemplateValue(eventCategory),
fileName: getFieldTemplateValue(fileName, true),
hostName: getFieldTemplateValue(hostName),
processName: getFieldTemplateValue(processName, true),
processParentName: getFieldTemplateValue(processParentName, true),
sourceAddress: getFieldTemplateValue(sourceAddress, true),
sourcePort: getFieldTemplateValue(sourcePort, true),
userName: getFieldTemplateValue(userName),
hasFieldOfInterest: fieldPresenceTracker.hasFieldOfInterest, // Tracking if we have any fields to show the 'with' word
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.
},
});
};
export const buildReasonMessageForEqlAlert = (args: BuildReasonMessageArgs) =>
buildCommonReasonMessage({ ...args });
buildReasonMessageUtil({ ...args, type: 'eql' });
export const buildReasonMessageForMlAlert = (args: BuildReasonMessageArgs) =>
buildCommonReasonMessage({ ...args });
buildReasonMessageUtil({ ...args, type: 'ml' });
export const buildReasonMessageForQueryAlert = (args: BuildReasonMessageArgs) =>
buildCommonReasonMessage({ ...args });
buildReasonMessageUtil({ ...args, type: 'query' });
export const buildReasonMessageForThreatMatchAlert = (args: BuildReasonMessageArgs) =>
buildCommonReasonMessage({ ...args });
buildReasonMessageUtil({ ...args, type: 'threatMatch' });
export const buildReasonMessageForThresholdAlert = (args: BuildReasonMessageArgs) =>
buildCommonReasonMessage({ ...args });
buildReasonMessageUtil({ ...args, type: 'threshold' });

View file

@ -193,7 +193,7 @@ export default ({ getService }: FtrProviderContext) => {
index: '.ml-anomalies-custom-linux_anomalous_network_activity_ecs',
depth: 0,
},
reason: `Alert Test ML rule created with a critical severity and risk score of 50 by root on mothra.`,
reason: `event with process store, by root on mothra created critical alert Test ML rule.`,
original_time: '2020-11-16T22:58:08.000Z',
},
all_field_values: [

View file

@ -287,7 +287,8 @@ export default ({ getService }: FtrProviderContext) => {
depth: 0,
},
],
reason: `Alert Query with a rule id created with a high severity and risk score of 55 by root on zeek-sensor-amsterdam.`,
reason:
'user-login event by root on zeek-sensor-amsterdam created high alert Query with a rule id.',
rule: fullSignal.signal.rule,
status: 'open',
},

View file

@ -362,7 +362,8 @@ export default ({ getService }: FtrProviderContext) => {
},
},
signal: {
reason: `Alert Signal Testing Query created with a high severity and risk score of 1 on suricata-zeek-sensor-toronto.`,
reason:
'configuration event on suricata-zeek-sensor-toronto created high alert Signal Testing Query.',
rule: fullSignal.signal.rule,
original_time: fullSignal.signal.original_time,
status: 'open',
@ -497,7 +498,8 @@ export default ({ getService }: FtrProviderContext) => {
},
},
signal: {
reason: `Alert Signal Testing Query created with a high severity and risk score of 1 on suricata-zeek-sensor-toronto.`,
reason:
'configuration event on suricata-zeek-sensor-toronto created high alert Signal Testing Query.',
rule: fullSignal.signal.rule,
original_time: fullSignal.signal.original_time,
status: 'open',
@ -662,7 +664,8 @@ export default ({ getService }: FtrProviderContext) => {
},
},
signal: {
reason: `Alert Signal Testing Query created with a high severity and risk score of 1 by root on zeek-sensor-amsterdam.`,
reason:
'anomoly event with process bro, by root on zeek-sensor-amsterdam created high alert Signal Testing Query.',
rule: fullSignal.signal.rule,
group: fullSignal.signal.group,
original_time: fullSignal.signal.original_time,
@ -753,7 +756,8 @@ export default ({ getService }: FtrProviderContext) => {
status: 'open',
depth: 2,
group: source.signal.group,
reason: `Alert Signal Testing Query created with a high severity and risk score of 1.`,
reason:
'event by root on zeek-sensor-amsterdam created high alert Signal Testing Query.',
rule: source.signal.rule,
ancestors: [
{
@ -872,7 +876,7 @@ export default ({ getService }: FtrProviderContext) => {
},
],
status: 'open',
reason: `Alert Signal Testing Query created with a high severity and risk score of 1.`,
reason: 'event created high alert Signal Testing Query.',
rule: fullSignal.signal.rule,
original_time: fullSignal.signal.original_time,
depth: 1,
@ -1010,7 +1014,7 @@ export default ({ getService }: FtrProviderContext) => {
},
],
status: 'open',
reason: `Alert Signal Testing Query created with a high severity and risk score of 1.`,
reason: `event created high alert Signal Testing Query.`,
rule: fullSignal.signal.rule,
original_time: fullSignal.signal.original_time,
depth: 1,
@ -1094,7 +1098,7 @@ export default ({ getService }: FtrProviderContext) => {
},
],
status: 'open',
reason: `Alert Signal Testing Query created with a high severity and risk score of 1.`,
reason: `event created high alert Signal Testing Query.`,
rule: fullSignal.signal.rule,
original_time: fullSignal.signal.original_time,
depth: 1,
@ -1692,7 +1696,7 @@ export default ({ getService }: FtrProviderContext) => {
},
],
status: 'open',
reason: `Alert boot created with a high severity and risk score of 1 on zeek-sensor-amsterdam.`,
reason: `event on zeek-sensor-amsterdam created high alert boot.`,
rule: {
...fullSignal.signal.rule,
name: 'boot',