[Security Solution][RAC] - Update reason field text (#110308)
This commit is contained in:
parent
f8e86f5e02
commit
3dda4dafa0
|
@ -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,
|
||||
|
|
|
@ -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(`""`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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' });
|
||||
|
|
|
@ -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: [
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
|
|
|
@ -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',
|
||||
|
|
Loading…
Reference in a new issue