Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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."`
);
});
});
Expand All @@ -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."`
);
});
});
Expand All @@ -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(`""`);
});
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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 fieldPresenceTracker = { hasFieldOfInterest: false };

const isFieldEmpty = (field: string | string[] | undefined | null) =>
!field || !field.length || (field.length === 1 && field[0] === '-');
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' });
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
},
Expand Down
Loading