diff --git a/src/platform/packages/shared/kbn-securitysolution-ecs/src/dll/index.ts b/src/platform/packages/shared/kbn-securitysolution-ecs/src/dll/index.ts index 956ef7bc9ab99..e286dd65ba55d 100644 --- a/src/platform/packages/shared/kbn-securitysolution-ecs/src/dll/index.ts +++ b/src/platform/packages/shared/kbn-securitysolution-ecs/src/dll/index.ts @@ -7,10 +7,11 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import type { CodeSignature } from '../file'; +import type { CodeSignature, Ext } from '../file'; import type { ProcessPe } from '../process'; export interface DllEcs { + Ext?: Ext; path?: string; code_signature?: CodeSignature; pe?: ProcessPe; diff --git a/src/platform/packages/shared/kbn-securitysolution-ecs/src/file/index.ts b/src/platform/packages/shared/kbn-securitysolution-ecs/src/file/index.ts index daf4226679591..ba6a6542e18e8 100644 --- a/src/platform/packages/shared/kbn-securitysolution-ecs/src/file/index.ts +++ b/src/platform/packages/shared/kbn-securitysolution-ecs/src/file/index.ts @@ -14,7 +14,7 @@ interface Original { export interface CodeSignature { subject_name: string[]; - trusted: string[]; + trusted: boolean; } export interface Token { @@ -72,6 +72,8 @@ export interface FileEcs { type?: string[]; + code_signature?: CodeSignature; + device?: string[]; inode?: string[]; diff --git a/x-pack/solutions/security/plugins/security_solution/common/endpoint/generate_data.ts b/x-pack/solutions/security/plugins/security_solution/common/endpoint/generate_data.ts index b4ee20f3d5ba2..486d48d465ed2 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/endpoint/generate_data.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/endpoint/generate_data.ts @@ -529,6 +529,10 @@ export class EndpointDocGenerator extends BaseDataGenerator { trusted: false, subject_name: 'bad signer', }, + { + trusted: true, + subject_name: 'a good signer', + }, ], malware_classification: { identifier: 'endpointpe', @@ -900,6 +904,10 @@ export class EndpointDocGenerator extends BaseDataGenerator { trusted: false, subject_name: 'bad signer', }, + { + trusted: true, + subject_name: 'good signer', + }, ], user: 'SYSTEM', token: { @@ -921,36 +929,34 @@ export class EndpointDocGenerator extends BaseDataGenerator { * Returns the default DLLs used in alerts */ private getAlertsDefaultDll() { - return [ - { - pe: { - architecture: 'x64', - }, - code_signature: { - subject_name: 'Cybereason Inc', - trusted: true, - }, + return { + pe: { + architecture: 'x64', + }, + code_signature: { + subject_name: 'Cybereason Inc', + trusted: true, + }, - hash: { - md5: '1f2d082566b0fc5f2c238a5180db7451', - sha1: 'ca85243c0af6a6471bdaa560685c51eefd6dbc0d', - sha256: '8ad40c90a611d36eb8f9eb24fa04f7dbca713db383ff55a03aa0f382e92061a2', - }, + hash: { + md5: '1f2d082566b0fc5f2c238a5180db7451', + sha1: 'ca85243c0af6a6471bdaa560685c51eefd6dbc0d', + sha256: '8ad40c90a611d36eb8f9eb24fa04f7dbca713db383ff55a03aa0f382e92061a2', + }, - path: 'C:\\Program Files\\Cybereason ActiveProbe\\AmSvc.exe', - Ext: { - compile_time: 1534424710, - mapped_address: 5362483200, - mapped_size: 0, - malware_classification: { - identifier: 'Whitelisted', - score: 0, - threshold: 0, - version: '3.0.0', - }, + path: 'C:\\Program Files\\Cybereason ActiveProbe\\AmSvc.exe', + Ext: { + compile_time: 1534424710, + mapped_address: 5362483200, + mapped_size: 0, + malware_classification: { + identifier: 'Whitelisted', + score: 0, + threshold: 0, + version: '3.0.0', }, }, - ]; + }; } /** diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_exceptions/utils/helpers.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_exceptions/utils/helpers.test.tsx index d404cba0257a2..412c2043e43de 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_exceptions/utils/helpers.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_exceptions/utils/helpers.test.tsx @@ -22,6 +22,7 @@ import { defaultEndpointExceptionItems, getFileCodeSignature, getProcessCodeSignature, + getDllCodeSignature, retrieveAlertOsTypes, getCodeSignatureValue, buildRuleExceptionWithConditions, @@ -249,13 +250,40 @@ describe('Exception helpers', () => { }); describe('#getCodeSignatureValue', () => { - test('it should return empty string if code_signature nested value are undefined', () => { + test('it should return undefined if code_signature nested value are undefined', () => { // Using the unsafe casting because with our types this shouldn't be possible but there have been issues with old data having undefined values in these fields const payload = [{ trusted: undefined, subject_name: undefined }] as unknown as Flattened< CodeSignature[] >; - const result = getCodeSignatureValue(payload); - expect(result).toEqual([{ trusted: '', subjectName: '' }]); + const result = getCodeSignatureValue(payload, 'field'); + expect(result).toEqual([undefined]); + }); + + test('it should not return duplicate code signature entries', () => { + const payload = [ + { subject_name: 'asdf', trusted: true }, + { subject_name: 'asdf', trusted: true }, + ]; + expect(getCodeSignatureValue(payload, 'field')).toEqual([ + { + field: 'field', + type: 'nested', + entries: [ + { + field: 'subject_name', + operator: 'included', + type: 'match', + value: 'asdf', + }, + { + field: 'trusted', + operator: 'included', + type: 'match', + value: 'true', + }, + ], + }, + ]); }); }); @@ -380,56 +408,26 @@ describe('Exception helpers', () => { _id: 'test-id', file: { path: 'some-file-path', hash: { sha256: 'some-hash' } }, }; - test('it returns prepopulated fields with empty values', () => { + test('it does not return prepopulated fields with empty values', () => { const prepopulatedItem = getPrepopulatedEndpointException({ listId: 'some_id', name: 'my rule', - codeSignature: { subjectName: '', trusted: '' }, eventCode: '', alertEcsData: { ...alertDataMock, file: { path: '', hash: { sha256: '' } } }, }); - expect(prepopulatedItem.entries).toEqual([ - { - entries: [ - { id: '123', field: 'subject_name', operator: 'included', type: 'match', value: '' }, - { id: '123', field: 'trusted', operator: 'included', type: 'match', value: '' }, - ], - field: 'file.Ext.code_signature', - type: 'nested', - id: '123', - }, - { id: '123', field: 'file.path.caseless', operator: 'included', type: 'match', value: '' }, - { id: '123', field: 'file.hash.sha256', operator: 'included', type: 'match', value: '' }, - { id: '123', field: 'event.code', operator: 'included', type: 'match', value: '' }, - ]); + expect(prepopulatedItem.entries).toEqual([]); }); test('it returns prepopulated items with actual values', () => { const prepopulatedItem = getPrepopulatedEndpointException({ listId: 'some_id', name: 'my rule', - codeSignature: { subjectName: 'someSubjectName', trusted: 'false' }, eventCode: 'some-event-code', alertEcsData: alertDataMock, }); expect(prepopulatedItem.entries).toEqual([ - { - entries: [ - { - id: '123', - field: 'subject_name', - operator: 'included', - type: 'match', - value: 'someSubjectName', - }, - { id: '123', field: 'trusted', operator: 'included', type: 'match', value: 'false' }, - ], - field: 'file.Ext.code_signature', - type: 'nested', - id: '123', - }, { id: '123', field: 'file.path.caseless', @@ -463,13 +461,32 @@ describe('Exception helpers', () => { Ext: { code_signature: { subject_name: 'some_subject', - trusted: 'false', + trusted: true, }, }, }, }); - expect(codeSignatures).toEqual([{ subjectName: 'some_subject', trusted: 'false' }]); + expect(codeSignatures).toEqual([ + { + field: 'file.Ext.code_signature', + type: 'nested', + entries: [ + { + field: 'subject_name', + operator: 'included', + type: 'match', + value: 'some_subject', + }, + { + field: 'trusted', + operator: 'included', + type: 'match', + value: 'true', + }, + ], + }, + ]); }); test('it works when file.Ext.code_signature is nested type', () => { @@ -478,36 +495,112 @@ describe('Exception helpers', () => { file: { Ext: { code_signature: [ - { subject_name: 'some_subject', trusted: 'false' }, - { subject_name: 'some_subject_2', trusted: 'true' }, + { subject_name: 'some_subject', trusted: true }, + { subject_name: 'some_subject_2', trusted: true }, ], }, }, }); expect(codeSignatures).toEqual([ - { subjectName: 'some_subject', trusted: 'false' }, { - subjectName: 'some_subject_2', - trusted: 'true', + field: 'file.Ext.code_signature', + type: 'nested', + entries: [ + { + field: 'subject_name', + operator: 'included', + type: 'match', + value: 'some_subject', + }, + { + field: 'trusted', + operator: 'included', + type: 'match', + value: 'true', + }, + ], + }, + { + field: 'file.Ext.code_signature', + type: 'nested', + entries: [ + { + field: 'subject_name', + operator: 'included', + type: 'match', + value: 'some_subject_2', + }, + { + field: 'trusted', + operator: 'included', + type: 'match', + value: 'true', + }, + ], }, ]); }); - test('it returns default when file.Ext.code_signatures values are empty', () => { - const codeSignatures = getFileCodeSignature({ + test('it returns an exception entry when file.Ext.code_signature is undefined but file.code_signature is defined', () => { + const codeSignature = getFileCodeSignature({ + _id: '123', + file: { + code_signature: { subject_name: 'some_subject', trusted: true }, + }, + }); + + expect(codeSignature).toEqual([ + { + field: 'file.code_signature.subject_name', + operator: 'included', + type: 'match', + value: 'some_subject', + }, + { + field: 'file.code_signature.trusted', + operator: 'included', + type: 'match', + value: 'true', + }, + ]); + }); + + test('it does not return an exception entry when code signature "trusted: false"', () => { + const extCodeSignatures = getFileCodeSignature({ _id: '123', file: { Ext: { - code_signature: { subject_name: '', trusted: '' }, + code_signature: [ + { subject_name: 'some_subject', trusted: false }, + { subject_name: 'some_subject_2', trusted: false }, + ], }, }, }); + const codeSignature = getFileCodeSignature({ + _id: '123', + file: { + code_signature: { subject_name: 'some_subject', trusted: false }, + }, + }); + + expect(extCodeSignatures).toEqual([]); + expect(codeSignature).toEqual(undefined); + }); + + test('it returns undefined when file.Ext.code_signatures values are empty', () => { + const codeSignatures = getFileCodeSignature({ + _id: '123', + file: { + Ext: {}, + }, + }); - expect(codeSignatures).toEqual([{ subjectName: '', trusted: '' }]); + expect(codeSignatures).toEqual(undefined); }); - test('it returns default when file.Ext.code_signatures is empty array', () => { + test('it returns undefined when file.Ext.code_signatures is empty array', () => { const codeSignatures = getFileCodeSignature({ _id: '123', file: { @@ -517,71 +610,166 @@ describe('Exception helpers', () => { }, }); - expect(codeSignatures).toEqual([{ subjectName: '', trusted: '' }]); + expect(codeSignatures).toEqual(undefined); }); - test('it returns default when file.Ext.code_signatures does not exist', () => { + test('it returns undefined when file.Ext.code_signatures does not exist', () => { const codeSignatures = getFileCodeSignature({ _id: '123', }); - expect(codeSignatures).toEqual([{ subjectName: '', trusted: '' }]); + expect(codeSignatures).toEqual(undefined); }); }); describe('getProcessCodeSignature', () => { - test('it works when file.Ext.code_signature is an object', () => { + test('it works when process.Ext.code_signature is an object', () => { const codeSignatures = getProcessCodeSignature({ _id: '123', process: { Ext: { code_signature: { subject_name: 'some_subject', - trusted: 'false', + trusted: true, }, }, }, }); - expect(codeSignatures).toEqual([{ subjectName: 'some_subject', trusted: 'false' }]); + expect(codeSignatures).toEqual([ + { + field: 'process.Ext.code_signature', + type: 'nested', + entries: [ + { + field: 'subject_name', + operator: 'included', + type: 'match', + value: 'some_subject', + }, + { + field: 'trusted', + operator: 'included', + type: 'match', + value: 'true', + }, + ], + }, + ]); }); - test('it works when file.Ext.code_signature is nested type', () => { + test('it works when process.Ext.code_signature is nested type', () => { const codeSignatures = getProcessCodeSignature({ _id: '123', process: { Ext: { code_signature: [ - { subject_name: 'some_subject', trusted: 'false' }, - { subject_name: 'some_subject_2', trusted: 'true' }, + { subject_name: 'some_subject', trusted: true }, + { subject_name: 'some_subject_2', trusted: true }, ], }, }, }); expect(codeSignatures).toEqual([ - { subjectName: 'some_subject', trusted: 'false' }, { - subjectName: 'some_subject_2', - trusted: 'true', + field: 'process.Ext.code_signature', + type: 'nested', + entries: [ + { + field: 'subject_name', + operator: 'included', + type: 'match', + value: 'some_subject', + }, + { + field: 'trusted', + operator: 'included', + type: 'match', + value: 'true', + }, + ], + }, + { + field: 'process.Ext.code_signature', + type: 'nested', + entries: [ + { + field: 'subject_name', + operator: 'included', + type: 'match', + value: 'some_subject_2', + }, + { + field: 'trusted', + operator: 'included', + type: 'match', + value: 'true', + }, + ], }, ]); }); - test('it returns default when file.Ext.code_signatures values are empty', () => { - const codeSignatures = getProcessCodeSignature({ + test('it returns an exception entry when process.Ext.code_signature is undefined but process.code_signature is defined', () => { + const codeSignature = getProcessCodeSignature({ + _id: '123', + process: { + code_signature: { subject_name: 'some_subject', trusted: true }, + }, + }); + + expect(codeSignature).toEqual([ + { + field: 'process.code_signature.subject_name', + operator: 'included', + type: 'match', + value: 'some_subject', + }, + { + field: 'process.code_signature.trusted', + operator: 'included', + type: 'match', + value: 'true', + }, + ]); + }); + + test('it does not return an exception entry when code signature "trusted: false"', () => { + const extCodeSignatures = getProcessCodeSignature({ _id: '123', process: { Ext: { - code_signature: { subject_name: '', trusted: '' }, + code_signature: [ + { subject_name: 'some_subject', trusted: false }, + { subject_name: 'some_subject_2', trusted: false }, + ], }, }, }); + const codeSignature = getProcessCodeSignature({ + _id: '123', + file: { + code_signature: { subject_name: 'some_subject', trusted: false }, + }, + }); - expect(codeSignatures).toEqual([{ subjectName: '', trusted: '' }]); + expect(extCodeSignatures).toEqual([]); + expect(codeSignature).toEqual(undefined); }); - test('it returns default when file.Ext.code_signatures is empty array', () => { + test('it returns undefined when process.Ext.code_signatures values are empty', () => { + const codeSignatures = getProcessCodeSignature({ + _id: '123', + process: { + Ext: {}, + }, + }); + + expect(codeSignatures).toEqual(undefined); + }); + + test('it returns undefined when process.Ext.code_signatures is empty array', () => { const codeSignatures = getProcessCodeSignature({ _id: '123', process: { @@ -591,15 +779,184 @@ describe('Exception helpers', () => { }, }); - expect(codeSignatures).toEqual([{ subjectName: '', trusted: '' }]); + expect(codeSignatures).toEqual(undefined); }); - test('it returns default when file.Ext.code_signatures does not exist', () => { + test('it returns undefined when process.Ext.code_signatures does not exist', () => { const codeSignatures = getProcessCodeSignature({ _id: '123', }); - expect(codeSignatures).toEqual([{ subjectName: '', trusted: '' }]); + expect(codeSignatures).toEqual(undefined); + }); + }); + + describe('getDllCodeSignature', () => { + test('it works when dll.Ext.code_signature is an object', () => { + const codeSignatures = getDllCodeSignature({ + _id: '123', + dll: { + Ext: { + code_signature: { + subject_name: 'some_subject', + trusted: true, + }, + }, + }, + }); + + expect(codeSignatures).toEqual([ + { + field: 'dll.Ext.code_signature', + type: 'nested', + entries: [ + { + field: 'subject_name', + operator: 'included', + type: 'match', + value: 'some_subject', + }, + { + field: 'trusted', + operator: 'included', + type: 'match', + value: 'true', + }, + ], + }, + ]); + }); + + test('it works when dll.Ext.code_signature is nested type', () => { + const codeSignatures = getDllCodeSignature({ + _id: '123', + dll: { + Ext: { + code_signature: [ + { subject_name: 'some_subject', trusted: true }, + { subject_name: 'some_subject_2', trusted: true }, + ], + }, + }, + }); + + expect(codeSignatures).toEqual([ + { + field: 'dll.Ext.code_signature', + type: 'nested', + entries: [ + { + field: 'subject_name', + operator: 'included', + type: 'match', + value: 'some_subject', + }, + { + field: 'trusted', + operator: 'included', + type: 'match', + value: 'true', + }, + ], + }, + { + field: 'dll.Ext.code_signature', + type: 'nested', + entries: [ + { + field: 'subject_name', + operator: 'included', + type: 'match', + value: 'some_subject_2', + }, + { + field: 'trusted', + operator: 'included', + type: 'match', + value: 'true', + }, + ], + }, + ]); + }); + + test('it returns an exception entry when dll.Ext.code_signature is undefined but dll.code_signature is defined', () => { + const codeSignature = getDllCodeSignature({ + _id: '123', + dll: { + code_signature: { subject_name: 'some_subject', trusted: true }, + }, + }); + + expect(codeSignature).toEqual([ + { + field: 'dll.code_signature.subject_name', + operator: 'included', + type: 'match', + value: 'some_subject', + }, + { + field: 'dll.code_signature.trusted', + operator: 'included', + type: 'match', + value: 'true', + }, + ]); + }); + + test('it does not return an exception entry when code signature "trusted: false"', () => { + const extCodeSignatures = getDllCodeSignature({ + _id: '123', + dll: { + Ext: { + code_signature: [ + { subject_name: 'some_subject', trusted: false }, + { subject_name: 'some_subject_2', trusted: false }, + ], + }, + }, + }); + const codeSignature = getDllCodeSignature({ + _id: '123', + file: { + code_signature: { subject_name: 'some_subject', trusted: false }, + }, + }); + + expect(extCodeSignatures).toEqual([]); + expect(codeSignature).toEqual(undefined); + }); + + test('it returns undefined when dll.Ext.code_signatures values are empty', () => { + const codeSignatures = getDllCodeSignature({ + _id: '123', + dll: { + Ext: {}, + }, + }); + + expect(codeSignatures).toEqual(undefined); + }); + + test('it returns undefined when dll.Ext.code_signatures is empty array', () => { + const codeSignatures = getDllCodeSignature({ + _id: '123', + dll: { + Ext: { + code_signature: [], + }, + }, + }); + + expect(codeSignatures).toEqual(undefined); + }); + + test('it returns undefined when dll.Ext.code_signatures does not exist', () => { + const codeSignatures = getDllCodeSignature({ + _id: '123', + }); + + expect(codeSignatures).toEqual(undefined); }); }); @@ -610,8 +967,8 @@ describe('Exception helpers', () => { file: { Ext: { code_signature: [ - { subject_name: 'some_subject', trusted: 'false' }, - { subject_name: 'some_subject_2', trusted: 'true' }, + { subject_name: 'some_subject', trusted: false }, + { subject_name: 'some_subject_2', trusted: true }, ], }, path: 'some file path', @@ -626,21 +983,6 @@ describe('Exception helpers', () => { }); expect(defaultItems[0].entries).toEqual([ - { - entries: [ - { - id: '123', - field: 'subject_name', - operator: 'included', - type: 'match', - value: 'some_subject', - }, - { id: '123', field: 'trusted', operator: 'included', type: 'match', value: 'false' }, - ], - field: 'file.Ext.code_signature', - type: 'nested', - id: '123', - }, { id: '123', field: 'file.path.caseless', @@ -662,8 +1004,6 @@ describe('Exception helpers', () => { type: 'match', value: 'some event code', }, - ]); - expect(defaultItems[1].entries).toEqual([ { entries: [ { @@ -679,9 +1019,75 @@ describe('Exception helpers', () => { type: 'nested', id: '123', }, + ]); + }); + + test('it should skip empty fields and "trusted:false" code signature fields"', () => { + const defaultItems = defaultEndpointExceptionItems('list_id', 'my_rule', { + _id: '123', + file: { + Ext: { + code_signature: [{ subject_name: 'some_subject', trusted: false }], + }, + path: '', + hash: { + sha256: 'some hash', + }, + }, + event: { + code: 'some event code', + }, + 'event.code': 'some event code', + }); + + expect(defaultItems[0].entries).toEqual([ + { + id: '123', + field: 'file.hash.sha256', + operator: 'included', + type: 'match', + value: 'some hash', + }, { id: '123', - field: 'file.path.caseless', + field: 'event.code', + operator: 'included', + type: 'match', + value: 'some event code', + }, + ]); + }); + + test('it should not return code signature fields for linux hosts', () => { + const defaultItems = defaultEndpointExceptionItems('list_id', 'my_rule', { + _id: '123', + host: { + os: { + name: 'Linux', + }, + }, + file: { + Ext: { + code_signature: [ + { subject_name: 'some_subject', trusted: true }, + { subject_name: 'some_subject_2', trusted: true }, + ], + }, + path: 'some file path', + hash: { + sha256: 'some hash', + }, + }, + event: { + code: 'some event code', + }, + 'event.code': 'some event code', + }); + + expect(defaultItems[0].entries).toEqual([ + { + id: '123', + field: 'file.path', operator: 'included', type: 'match', value: 'some file path', @@ -710,8 +1116,8 @@ describe('Exception helpers', () => { process: { Ext: { code_signature: [ - { subject_name: 'some_subject', trusted: 'false' }, - { subject_name: 'some_subject_2', trusted: 'true' }, + { subject_name: 'some_subject', trusted: false }, + { subject_name: 'some_subject_2', trusted: true }, ], }, executable: 'some file path', @@ -729,6 +1135,34 @@ describe('Exception helpers', () => { }); expect(defaultItems[0].entries).toEqual([ + { + id: '123', + field: 'process.executable', + operator: 'included', + type: 'match', + value: 'some file path', + }, + { + id: '123', + field: 'process.hash.sha256', + operator: 'included', + type: 'match', + value: 'some hash', + }, + { + id: '123', + field: 'Ransomware.feature', + operator: 'included', + type: 'match', + value: 'some ransomware feature', + }, + { + id: '123', + field: 'event.code', + operator: 'included', + type: 'match', + value: 'ransomware', + }, { entries: [ { @@ -736,14 +1170,39 @@ describe('Exception helpers', () => { field: 'subject_name', operator: 'included', type: 'match', - value: 'some_subject', + value: 'some_subject_2', }, - { id: '123', field: 'trusted', operator: 'included', type: 'match', value: 'false' }, + { id: '123', field: 'trusted', operator: 'included', type: 'match', value: 'true' }, ], field: 'process.Ext.code_signature', type: 'nested', id: '123', }, + ]); + }); + + test('it should skip empty fields and "trusted:false" code signature fields', () => { + const defaultItems = defaultEndpointExceptionItems('list_id', 'my_rule', { + _id: '123', + process: { + Ext: { + code_signature: [{ subject_name: 'some_subject', trusted: false }], + }, + executable: 'some file path', + hash: { + sha256: '', + }, + }, + Ransomware: { + feature: 'some ransomware feature', + }, + event: { + code: 'ransomware', + }, + 'event.code': 'ransomware', + }); + + expect(defaultItems[0].entries).toEqual([ { id: '123', field: 'process.executable', @@ -753,42 +1212,48 @@ describe('Exception helpers', () => { }, { id: '123', - field: 'process.hash.sha256', + field: 'Ransomware.feature', operator: 'included', type: 'match', - value: 'some hash', + value: 'some ransomware feature', }, { id: '123', - field: 'Ransomware.feature', + field: 'event.code', operator: 'included', type: 'match', - value: 'some ransomware feature', + value: 'ransomware', + }, + ]); + }); + + it('should not return code signatures for linux hosts', () => { + const defaultItems = defaultEndpointExceptionItems('list_id', 'my_rule', { + _id: '123', + host: { + os: { + name: 'Linux', + }, + }, + process: { + Ext: { + code_signature: [{ subject_name: 'some_subject', trusted: true }], + }, + executable: 'some file path', + hash: { + sha256: 'some hash', + }, }, - { - id: '123', - field: 'event.code', - operator: 'included', - type: 'match', - value: 'ransomware', + Ransomware: { + feature: 'some ransomware feature', }, - ]); - expect(defaultItems[1].entries).toEqual([ - { - entries: [ - { - id: '123', - field: 'subject_name', - operator: 'included', - type: 'match', - value: 'some_subject_2', - }, - { id: '123', field: 'trusted', operator: 'included', type: 'match', value: 'true' }, - ], - field: 'process.Ext.code_signature', - type: 'nested', - id: '123', + event: { + code: 'ransomware', }, + 'event.code': 'ransomware', + }); + + expect(defaultItems[0].entries).toEqual([ { id: '123', field: 'process.executable', @@ -1077,7 +1542,7 @@ describe('Exception helpers', () => { }, code_signature: { subject_name: 'subject-name', - trusted: 'true', + trusted: true, }, }, event: { @@ -1105,7 +1570,7 @@ describe('Exception helpers', () => { path: 'dll-path', code_signature: { subject_name: 'dll-code-signature-subject-name', - trusted: 'false', + trusted: true, }, pe: { original_file_name: 'dll-pe-original-file-name', @@ -1151,13 +1616,6 @@ describe('Exception helpers', () => { type: 'match' as const, value: 'parent file path', }, - { - id: '123', - field: 'process.code_signature.subject_name', - operator: 'included' as const, - type: 'match' as const, - value: 'subject-name', - }, { id: '123', field: 'file.path', @@ -1214,13 +1672,6 @@ describe('Exception helpers', () => { type: 'match' as const, value: 'dll-path', }, - { - id: '123', - field: 'dll.code_signature.subject_name', - operator: 'included' as const, - type: 'match' as const, - value: 'dll-code-signature-subject-name', - }, { id: '123', field: 'dll.pe.original_file_name', @@ -1249,9 +1700,37 @@ describe('Exception helpers', () => { type: 'match' as const, value: '0987', }, + { + id: '123', + field: 'process.code_signature.subject_name', + operator: 'included' as const, + type: 'match' as const, + value: 'subject-name', + }, + { + id: '123', + field: 'process.code_signature.trusted', + operator: 'included' as const, + type: 'match' as const, + value: 'true', + }, + { + id: '123', + field: 'dll.code_signature.subject_name', + operator: 'included' as const, + type: 'match' as const, + value: 'dll-code-signature-subject-name', + }, + { + id: '123', + field: 'dll.code_signature.trusted', + operator: 'included' as const, + type: 'match' as const, + value: 'true', + }, ]); }); - test('it should return pre-populated behavior protection fields and skip empty', () => { + test('it should skip empty fields and "trusted: false" code signature fields', () => { const defaultItems = defaultEndpointExceptionItems('list_id', 'my_rule', { _id: '123', rule: { @@ -1265,7 +1744,7 @@ describe('Exception helpers', () => { }, code_signature: { subject_name: 'subject-name', - trusted: 'true', + trusted: true, }, }, event: { @@ -1294,7 +1773,7 @@ describe('Exception helpers', () => { path: 'dll-path', code_signature: { subject_name: 'dll-code-signature-subject-name', - trusted: 'false', + trusted: false, }, pe: { original_file_name: 'dll-pe-original-file-name', @@ -1333,6 +1812,62 @@ describe('Exception helpers', () => { type: 'match' as const, value: 'parent file path', }, + { + id: '123', + field: 'file.name', + operator: 'included' as const, + type: 'match' as const, + value: 'fake-file-name', + }, + { + id: '123', + field: 'source.ip', + operator: 'included' as const, + type: 'match' as const, + value: '0.0.0.0', + }, + { + id: '123', + field: 'destination.ip', + operator: 'included' as const, + type: 'match' as const, + value: '0.0.0.0', + }, + { + id: '123', + field: 'dll.path', + operator: 'included' as const, + type: 'match' as const, + value: 'dll-path', + }, + { + id: '123', + field: 'dll.pe.original_file_name', + operator: 'included' as const, + type: 'match' as const, + value: 'dll-pe-original-file-name', + }, + { + id: '123', + field: 'dns.question.name', + operator: 'included' as const, + type: 'match' as const, + value: 'dns-question-name', + }, + { + id: '123', + field: 'dns.question.type', + operator: 'included' as const, + type: 'match' as const, + value: 'dns-question-type', + }, + { + id: '123', + field: 'user.id', + operator: 'included' as const, + type: 'match' as const, + value: '0987', + }, { id: '123', field: 'process.code_signature.subject_name', @@ -1340,6 +1875,116 @@ describe('Exception helpers', () => { type: 'match' as const, value: 'subject-name', }, + { + id: '123', + field: 'process.code_signature.trusted', + operator: 'included' as const, + type: 'match' as const, + value: 'true', + }, + ]); + }); + + test('it should not return code signature fields for linux hosts', () => { + const defaultItems = defaultEndpointExceptionItems('list_id', 'my_rule', { + _id: '123', + rule: { + id: '123', + }, + host: { + os: { + name: 'Linux', + }, + }, + process: { + command_line: 'command_line', + executable: 'some file path', + parent: { + executable: 'parent file path', + }, + code_signature: { + subject_name: 'subject-name', + trusted: true, + }, + }, + event: { + code: 'behavior', + }, + 'event.code': 'behavior', + file: { + path: 'fake-file-path', + name: 'fake-file-name', + }, + source: { + ip: '0.0.0.0', + }, + destination: { + ip: '0.0.0.0', + }, + registry: { + path: 'registry-path', + value: 'registry-value', + data: { + strings: 'registry-strings', + }, + }, + dll: { + path: 'dll-path', + code_signature: { + subject_name: 'dll-code-signature-subject-name', + trusted: true, + }, + pe: { + original_file_name: 'dll-pe-original-file-name', + }, + }, + dns: { + question: { + name: 'dns-question-name', + type: 'dns-question-type', + }, + }, + user: { + id: '0987', + }, + }); + + expect(defaultItems[0].entries).toEqual([ + { + id: '123', + field: 'rule.id', + operator: 'included' as const, + type: 'match' as const, + value: '123', + }, + { + id: '123', + field: 'process.executable.caseless', + operator: 'included' as const, + type: 'match' as const, + value: 'some file path', + }, + { + id: '123', + field: 'process.command_line', + operator: 'included' as const, + type: 'match' as const, + value: 'command_line', + }, + { + id: '123', + field: 'process.parent.executable', + operator: 'included' as const, + type: 'match' as const, + value: 'parent file path', + }, + { + id: '123', + field: 'file.path', + operator: 'included' as const, + type: 'match' as const, + value: 'fake-file-path', + }, { id: '123', field: 'file.name', @@ -1363,17 +2008,31 @@ describe('Exception helpers', () => { }, { id: '123', - field: 'dll.path', + field: 'registry.path', operator: 'included' as const, type: 'match' as const, - value: 'dll-path', + value: 'registry-path', }, { id: '123', - field: 'dll.code_signature.subject_name', + field: 'registry.value', operator: 'included' as const, type: 'match' as const, - value: 'dll-code-signature-subject-name', + value: 'registry-value', + }, + { + id: '123', + field: 'registry.data.strings', + operator: 'included' as const, + type: 'match' as const, + value: 'registry-strings', + }, + { + id: '123', + field: 'dll.path', + operator: 'included' as const, + type: 'match' as const, + value: 'dll-path', }, { id: '123', diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_exceptions/utils/helpers.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_exceptions/utils/helpers.tsx index d879b79a042f7..0953c35f8bd42 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_exceptions/utils/helpers.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_exceptions/utils/helpers.tsx @@ -26,6 +26,7 @@ import type { UpdateExceptionListItemSchema, ExceptionListSchema, EntriesArray, + EntriesArrayOrUndefined, } from '@kbn/securitysolution-io-ts-list-types'; import { ListOperatorTypeEnum, @@ -41,11 +42,17 @@ import type { import { getNewExceptionItem, addIdToEntries } from '@kbn/securitysolution-list-utils'; import { removeIdFromExceptionItemsEntries } from '@kbn/securitysolution-list-hooks'; -import type { EcsSecurityExtension as Ecs, CodeSignature } from '@kbn/securitysolution-ecs'; +import type { + EcsSecurityExtension as Ecs, + CodeSignature, + FileEcs, + DllEcs, + ProcessEcs, +} from '@kbn/securitysolution-ecs'; import type { EventSummaryField } from '../../../common/components/event_details/types'; import { getHighlightedFieldsToDisplay } from '../../../common/components/event_details/get_alert_summary_rows'; import * as i18n from './translations'; -import type { AlertData, Flattened } from './types'; +import type { AlertData, Flattened, FlattenedCodeSignature } from './types'; import { WithCopyToClipboard } from '../../../common/lib/clipboard/with_copy_to_clipboard'; import { ALERT_ORIGINAL_EVENT } from '../../../../common/field_maps/field_names'; @@ -292,78 +299,150 @@ export const lowercaseHashValues = ( }; /** - * Returns the value for `file.Ext.code_signature` which - * can be an object or array of objects + * Generic function to get code signature entries from any entity */ -export const getFileCodeSignature = ( - alertData: Flattened -): Array<{ subjectName: string; trusted: string }> => { - const { file } = alertData; - const codeSignature = file && file.Ext && file.Ext.code_signature; +export const getEntityCodeSignature = < + T extends { + Ext?: { code_signature?: Flattened }; + code_signature?: CodeSignature; + } +>( + entity: Flattened | T | undefined, + fieldPrefix: string +): EntriesArrayOrUndefined => { + if (!entity) return undefined; - return getCodeSignatureValue(codeSignature); + // Check Ext.code_signature first + if (entity.Ext?.code_signature) { + return getCodeSignatureValue(entity.Ext.code_signature, `${fieldPrefix}.Ext.code_signature`); + } + + // Then check direct code_signature + if (entity.code_signature?.trusted === true) { + return [ + { + field: `${fieldPrefix}.code_signature.subject_name`, + operator: 'included' as const, + type: 'match' as const, + value: entity.code_signature?.subject_name.toString() ?? '', + }, + { + field: `${fieldPrefix}.code_signature.trusted`, + operator: 'included' as const, + type: 'match' as const, + value: entity.code_signature.trusted.toString(), + }, + ]; + } + return undefined; }; /** - * Returns the value for `process.Ext.code_signature` which - * can be an object or array of objects + * Returns an array of exception entries for either + * `file.Ext.code_signature` or 'file.code_signature` + * as long as the `trusted` field is `true`. */ -export const getProcessCodeSignature = ( - alertData: Flattened -): Array<{ subjectName: string; trusted: string }> => { - const { process } = alertData; - const codeSignature = process && process.Ext && process.Ext.code_signature; - return getCodeSignatureValue(codeSignature); -}; +export const getFileCodeSignature = (alertData: Flattened): EntriesArrayOrUndefined => + getEntityCodeSignature(alertData.file, 'file'); + +/** + * Returns an array of exception entries for either + * `process.Ext.code_signature` or 'process.code_signature` + * as long as the `trusted` field is `true`. + */ +export const getProcessCodeSignature = (alertData: Flattened): EntriesArrayOrUndefined => + getEntityCodeSignature(alertData.process, 'process'); + +/** + * Returns an array of exception entries for either + * `dll.Ext.code_signature` or 'dll.code_signature` + * as long as the `trusted` field is `true`. + */ +export const getDllCodeSignature = (alertData: Flattened): EntriesArrayOrUndefined => + getEntityCodeSignature(alertData.dll, 'dll'); /** * Pre 7.10 `Ext.code_signature` fields were mistakenly populated as * a single object with subject_name and trusted. */ export const getCodeSignatureValue = ( - codeSignature: Flattened | Flattened | undefined -): Array<{ subjectName: string; trusted: string }> => { + codeSignature: Flattened | FlattenedCodeSignature[] | undefined, + field: string +): EntryNested[] | undefined => { if (Array.isArray(codeSignature) && codeSignature.length > 0) { - return codeSignature.map((signature) => { - return { - subjectName: signature?.subject_name ?? '', - trusted: signature?.trusted?.toString() ?? '', - }; - }); + const codeSignatureEntries: EntryNested[] = []; + const noDuplicates = new Map(); + return codeSignature.reduce((acc, signature) => { + if (signature?.trusted === true && !noDuplicates.has(signature?.subject_name)) { + noDuplicates.set(signature.subject_name, signature.trusted); + acc.push({ + field, + type: 'nested', + entries: [ + { + field: 'subject_name', + operator: 'included', + type: 'match', + value: signature?.subject_name ?? '', + }, + { + field: 'trusted', + operator: 'included', + type: 'match', + value: signature.trusted.toString(), + }, + ], + }); + } + return acc; + }, codeSignatureEntries); } else { const signature: Flattened | undefined = !Array.isArray(codeSignature) ? codeSignature : undefined; - - return [ - { - subjectName: signature?.subject_name ?? '', - trusted: signature?.trusted ?? '', - }, - ]; + if (signature?.trusted === true) { + return [ + { + field, + type: 'nested', + entries: [ + { + field: 'subject_name', + operator: 'included', + type: 'match', + value: signature?.subject_name ?? '', + }, + { + field: 'trusted', + operator: 'included', + type: 'match', + value: signature.trusted.toString(), + }, + ], + }, + ]; + } } }; -// helper type to filter empty-valued exception entries -interface ExceptionEntry { - value?: string; - entries?: ExceptionEntry[]; -} - /** * Takes an array of Entries and filter out the ones with empty values. * It will also filter out empty values for nested entries. */ -function filterEmptyExceptionEntries(entries: T[]): T[] { - const finalEntries: T[] = []; + +function filterEmptyExceptionEntries(entries: EntriesArray): EntriesArray { + const finalEntries: EntriesArray = []; for (const entry of entries) { - if (entry.entries !== undefined) { - entry.entries = entry.entries.filter((el) => el.value !== undefined && el.value.length > 0); + if ('entries' in entry && entry.entries !== undefined) { + entry.entries = entry.entries.filter( + (el) => 'value' in el && el.value !== undefined && el.value.length > 0 + ); finalEntries.push(entry); - } else if (entry.value !== undefined && entry.value.length > 0) { + } else if ('value' in entry && entry?.value?.length > 0) { finalEntries.push(entry); } } + return finalEntries; } @@ -373,7 +452,6 @@ function filterEmptyExceptionEntries(entries: T[]): T[ export const getPrepopulatedEndpointException = ({ listId, name, - codeSignature, eventCode, listNamespace = 'agnostic', alertEcsData, @@ -381,21 +459,16 @@ export const getPrepopulatedEndpointException = ({ listId: string; listNamespace?: NamespaceType; name: string; - codeSignature: { subjectName: string; trusted: string }; eventCode: string; alertEcsData: Flattened; }): ExceptionsBuilderExceptionItem => { const { file, host } = alertEcsData; + const fileCodeSignature = getFileCodeSignature(alertEcsData); const filePath = file?.path ?? ''; const sha256Hash = file?.hash?.sha256 ?? ''; const isLinux = host?.os?.name === 'Linux'; - const commonFields: Array<{ - field: string; - operator: 'excluded' | 'included'; - type: 'match'; - value: string; - }> = [ + const commonFields: EntriesArray = [ { field: isLinux ? 'file.path' : 'file.path.caseless', operator: 'included', @@ -416,30 +489,10 @@ export const getPrepopulatedEndpointException = ({ }, ]; const entriesToAdd = () => { - if (isLinux) { - return addIdToEntries(commonFields); + if (!isLinux && fileCodeSignature !== undefined) { + return addIdToEntries(filterEmptyExceptionEntries(commonFields.concat(fileCodeSignature))); } else { - return addIdToEntries([ - { - field: 'file.Ext.code_signature', - type: 'nested', - entries: [ - { - field: 'subject_name', - operator: 'included', - type: 'match', - value: codeSignature != null ? codeSignature.subjectName : '', - }, - { - field: 'trusted', - operator: 'included', - type: 'match', - value: codeSignature != null ? codeSignature.trusted : '', - }, - ], - }, - ...commonFields, - ]); + return addIdToEntries(filterEmptyExceptionEntries(commonFields)); } }; @@ -455,7 +508,6 @@ export const getPrepopulatedEndpointException = ({ export const getPrepopulatedRansomwareException = ({ listId, name, - codeSignature, eventCode, listNamespace = 'agnostic', alertEcsData, @@ -463,60 +515,54 @@ export const getPrepopulatedRansomwareException = ({ listId: string; listNamespace?: NamespaceType; name: string; - codeSignature: { subjectName: string; trusted: string }; eventCode: string; alertEcsData: Flattened; }): ExceptionsBuilderExceptionItem => { - const { process, Ransomware } = alertEcsData; + const { process, Ransomware, host } = alertEcsData; + const processCodeSignature = getProcessCodeSignature(alertEcsData); const sha256Hash = process?.hash?.sha256 ?? ''; const executable = process?.executable ?? ''; const ransomwareFeature = Ransomware?.feature ?? ''; + const isLinux = host?.os?.name === 'Linux'; + + const commonFields: EntriesArray = [ + { + field: 'process.executable', + operator: 'included', + type: 'match', + value: executable ?? '', + }, + { + field: 'process.hash.sha256', + operator: 'included', + type: 'match', + value: sha256Hash ?? '', + }, + { + field: 'Ransomware.feature', + operator: 'included', + type: 'match', + value: ransomwareFeature ?? '', + }, + { + field: 'event.code', + operator: 'included', + type: 'match', + value: eventCode ?? '', + }, + ]; + + const entriesToAdd = () => { + if (!isLinux && processCodeSignature !== undefined) { + return addIdToEntries(filterEmptyExceptionEntries(commonFields.concat(processCodeSignature))); + } else { + return addIdToEntries(filterEmptyExceptionEntries(commonFields)); + } + }; + return { ...getNewExceptionItem({ listId, namespaceType: listNamespace, name }), - entries: addIdToEntries([ - { - field: 'process.Ext.code_signature', - type: 'nested', - entries: [ - { - field: 'subject_name', - operator: 'included', - type: 'match', - value: codeSignature != null ? codeSignature.subjectName : '', - }, - { - field: 'trusted', - operator: 'included', - type: 'match', - value: codeSignature != null ? codeSignature.trusted : '', - }, - ], - }, - { - field: 'process.executable', - operator: 'included', - type: 'match', - value: executable ?? '', - }, - { - field: 'process.hash.sha256', - operator: 'included', - type: 'match', - value: sha256Hash ?? '', - }, - { - field: 'Ransomware.feature', - operator: 'included', - type: 'match', - value: ransomwareFeature ?? '', - }, - { - field: 'event.code', - operator: 'included', - type: 'match', - value: eventCode ?? '', - }, - ]), + entries: entriesToAdd(), }; }; @@ -618,6 +664,7 @@ export const getPrepopulatedMemoryShellcodeException = ({ }; }; +/* eslint complexity: ["error", 21]*/ export const getPrepopulatedBehaviorException = ({ listId, name, @@ -631,8 +678,11 @@ export const getPrepopulatedBehaviorException = ({ eventCode: string; alertEcsData: Flattened; }): ExceptionsBuilderExceptionItem => { - const { process } = alertEcsData; - const entries = filterEmptyExceptionEntries([ + const { process, host } = alertEcsData; + const processCodeSignature = getProcessCodeSignature(alertEcsData); + const dllCodeSignature = getDllCodeSignature(alertEcsData); + const isLinux = host?.os?.name === 'Linux'; + const commonFields: EntriesArray = [ { field: 'rule.id', operator: 'included' as const, @@ -657,12 +707,6 @@ export const getPrepopulatedBehaviorException = ({ type: 'match' as const, value: process?.parent?.executable ?? '', }, - { - field: 'process.code_signature.subject_name', - operator: 'included' as const, - type: 'match' as const, - value: process?.code_signature?.subject_name ?? '', - }, { field: 'file.path', operator: 'included' as const, @@ -711,12 +755,6 @@ export const getPrepopulatedBehaviorException = ({ type: 'match' as const, value: alertEcsData.dll?.path ?? '', }, - { - field: 'dll.code_signature.subject_name', - operator: 'included' as const, - type: 'match' as const, - value: alertEcsData.dll?.code_signature?.subject_name ?? '', - }, { field: 'dll.pe.original_file_name', operator: 'included' as const, @@ -741,10 +779,28 @@ export const getPrepopulatedBehaviorException = ({ type: 'match' as const, value: alertEcsData.user?.id ?? '', }, - ]); + ]; + + const entriesToAdd = () => { + if (!isLinux) { + if (processCodeSignature !== undefined && dllCodeSignature !== undefined) { + return addIdToEntries( + filterEmptyExceptionEntries(commonFields.concat(processCodeSignature, dllCodeSignature)) + ); + } else if (processCodeSignature !== undefined) { + return addIdToEntries( + filterEmptyExceptionEntries(commonFields.concat(processCodeSignature)) + ); + } else if (dllCodeSignature !== undefined) { + return addIdToEntries(filterEmptyExceptionEntries(commonFields.concat(dllCodeSignature))); + } + } + return addIdToEntries(filterEmptyExceptionEntries(commonFields)); + }; + return { ...getNewExceptionItem({ listId, namespaceType: listNamespace, name }), - entries: addIdToEntries(entries), + entries: entriesToAdd(), }; }; @@ -757,7 +813,6 @@ export const defaultEndpointExceptionItems = ( alertEcsData: Flattened & { 'event.code'?: string } ): ExceptionsBuilderExceptionItem[] => { const eventCode = alertEcsData['event.code'] ?? alertEcsData.event?.code; - switch (eventCode) { case 'behavior': return [ @@ -787,26 +842,24 @@ export const defaultEndpointExceptionItems = ( }), ]; case 'ransomware': - return getProcessCodeSignature(alertEcsData).map((codeSignature) => + return [ getPrepopulatedRansomwareException({ listId, name, eventCode, - codeSignature, alertEcsData, - }) - ); + }), + ]; default: // By default return the standard prepopulated Endpoint Exception fields - return getFileCodeSignature(alertEcsData).map((codeSignature) => + return [ getPrepopulatedEndpointException({ listId, name, eventCode: eventCode ?? '', - codeSignature, alertEcsData, - }) - ); + }), + ]; } }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_exceptions/utils/types.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_exceptions/utils/types.ts index 4e87e89b2964c..f6f6e5e29f8f1 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_exceptions/utils/types.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_exceptions/utils/types.ts @@ -23,7 +23,7 @@ export interface ExceptionsPagination { export interface FlattenedCodeSignature { subject_name: string; - trusted: string; + trusted: boolean; } export type Flattened = {