[Security Solution][Endpoint Exceptions] Fixes bug where behavior alerts do not show nested code signatures with subject name and trusted field (#212325)

## Summary

When navigating to the endpoint exceptions form from an alert, we
pre-populate certain exceptions fields based on the type of alert. There
was a bug for behavior alerts where we did not use the proper nested
`code_signature` field for windows and mac endpoints. Instead of showing
the nested `code_signature` field that has the `subject_name` and
`trusted` sub-fields, we only showed non-nested `code_signature subject
field. This PR also refactors the code to account for the following
behaviors that we want:
- [x] If `field.Ext.code_signature` is present, we want to use the
nested `code_signature` subject field with the `subject_name` and
`trusted` sub-fields for
- [x] If `field.Ext.code_signature` is not present, we will default to
the non-nested `field.code_signature.subject_name` and
`field.code_signature.trusted` field pair.
- [x] We will only show non-empty pre-populated values and also only
code signature values with the `trusted` field set to `true`
- [x] Pre-populated code signature fields are only present in windows
and mac OSes.
- [x] Behavior, ransomware and default alerts had the code_signature
adjustments
- [x] Previously the code duplicated a set of the pre-populated fields
PER code signature. Now, each pre-populated field is only shown once,
followed by all valid code_signatures.
- [x] Does not allow duplicate code signatures  



# SCREENSHOTS 

Behavior alert w/ nested `process.Ext.code_signature` and non-nested
`dll.code_signature` fields

![nested](https://github.com/user-attachments/assets/218f140e-21ee-40a5-8198-c37c474088a8)

Malware alert w/ nested `file.Ext.code_signature`
<img width="1281" alt="image"
src="https://github.com/user-attachments/assets/4845c6e5-5567-49df-b66a-1b9a2e6410db"
/>

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Candace Park 2025-05-08 13:22:42 -04:00 committed by GitHub
parent 8d50c08a43
commit 76e256ccff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 1036 additions and 315 deletions

View file

@ -7,10 +7,11 @@
* License v3.0 only", or the "Server Side Public License, v 1". * 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'; import type { ProcessPe } from '../process';
export interface DllEcs { export interface DllEcs {
Ext?: Ext;
path?: string; path?: string;
code_signature?: CodeSignature; code_signature?: CodeSignature;
pe?: ProcessPe; pe?: ProcessPe;

View file

@ -14,7 +14,7 @@ interface Original {
export interface CodeSignature { export interface CodeSignature {
subject_name: string[]; subject_name: string[];
trusted: string[]; trusted: boolean;
} }
export interface Token { export interface Token {
@ -72,6 +72,8 @@ export interface FileEcs {
type?: string[]; type?: string[];
code_signature?: CodeSignature;
device?: string[]; device?: string[];
inode?: string[]; inode?: string[];

View file

@ -529,6 +529,10 @@ export class EndpointDocGenerator extends BaseDataGenerator {
trusted: false, trusted: false,
subject_name: 'bad signer', subject_name: 'bad signer',
}, },
{
trusted: true,
subject_name: 'a good signer',
},
], ],
malware_classification: { malware_classification: {
identifier: 'endpointpe', identifier: 'endpointpe',
@ -900,6 +904,10 @@ export class EndpointDocGenerator extends BaseDataGenerator {
trusted: false, trusted: false,
subject_name: 'bad signer', subject_name: 'bad signer',
}, },
{
trusted: true,
subject_name: 'good signer',
},
], ],
user: 'SYSTEM', user: 'SYSTEM',
token: { token: {
@ -921,8 +929,7 @@ export class EndpointDocGenerator extends BaseDataGenerator {
* Returns the default DLLs used in alerts * Returns the default DLLs used in alerts
*/ */
private getAlertsDefaultDll() { private getAlertsDefaultDll() {
return [ return {
{
pe: { pe: {
architecture: 'x64', architecture: 'x64',
}, },
@ -949,8 +956,7 @@ export class EndpointDocGenerator extends BaseDataGenerator {
version: '3.0.0', version: '3.0.0',
}, },
}, },
}, };
];
} }
/** /**

View file

@ -26,6 +26,7 @@ import type {
UpdateExceptionListItemSchema, UpdateExceptionListItemSchema,
ExceptionListSchema, ExceptionListSchema,
EntriesArray, EntriesArray,
EntriesArrayOrUndefined,
} from '@kbn/securitysolution-io-ts-list-types'; } from '@kbn/securitysolution-io-ts-list-types';
import { import {
ListOperatorTypeEnum, ListOperatorTypeEnum,
@ -41,11 +42,17 @@ import type {
import { getNewExceptionItem, addIdToEntries } from '@kbn/securitysolution-list-utils'; import { getNewExceptionItem, addIdToEntries } from '@kbn/securitysolution-list-utils';
import { removeIdFromExceptionItemsEntries } from '@kbn/securitysolution-list-hooks'; 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 type { EventSummaryField } from '../../../common/components/event_details/types';
import { getHighlightedFieldsToDisplay } from '../../../common/components/event_details/get_alert_summary_rows'; import { getHighlightedFieldsToDisplay } from '../../../common/components/event_details/get_alert_summary_rows';
import * as i18n from './translations'; 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 { WithCopyToClipboard } from '../../../common/lib/clipboard/with_copy_to_clipboard';
import { ALERT_ORIGINAL_EVENT } from '../../../../common/field_maps/field_names'; 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 * Generic function to get code signature entries from any entity
* can be an object or array of objects
*/ */
export const getFileCodeSignature = ( export const getEntityCodeSignature = <
alertData: Flattened<Ecs> T extends {
): Array<{ subjectName: string; trusted: string }> => { Ext?: { code_signature?: Flattened<CodeSignature[] | CodeSignature> };
const { file } = alertData; code_signature?: CodeSignature;
const codeSignature = file && file.Ext && file.Ext.code_signature; }
>(
entity: Flattened<FileEcs | ProcessEcs | DllEcs> | 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 * Returns an array of exception entries for either
* can be an object or array of objects * `file.Ext.code_signature` or 'file.code_signature`
* as long as the `trusted` field is `true`.
*/ */
export const getProcessCodeSignature = ( export const getFileCodeSignature = (alertData: Flattened<Ecs>): EntriesArrayOrUndefined =>
alertData: Flattened<Ecs> getEntityCodeSignature(alertData.file, 'file');
): Array<{ subjectName: string; trusted: string }> => {
const { process } = alertData; /**
const codeSignature = process && process.Ext && process.Ext.code_signature; * Returns an array of exception entries for either
return getCodeSignatureValue(codeSignature); * `process.Ext.code_signature` or 'process.code_signature`
}; * as long as the `trusted` field is `true`.
*/
export const getProcessCodeSignature = (alertData: Flattened<Ecs>): 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<Ecs>): EntriesArrayOrUndefined =>
getEntityCodeSignature(alertData.dll, 'dll');
/** /**
* Pre 7.10 `Ext.code_signature` fields were mistakenly populated as * Pre 7.10 `Ext.code_signature` fields were mistakenly populated as
* a single object with subject_name and trusted. * a single object with subject_name and trusted.
*/ */
export const getCodeSignatureValue = ( export const getCodeSignatureValue = (
codeSignature: Flattened<CodeSignature> | Flattened<CodeSignature[]> | undefined codeSignature: Flattened<CodeSignature> | FlattenedCodeSignature[] | undefined,
): Array<{ subjectName: string; trusted: string }> => { field: string
): EntryNested[] | undefined => {
if (Array.isArray(codeSignature) && codeSignature.length > 0) { if (Array.isArray(codeSignature) && codeSignature.length > 0) {
return codeSignature.map((signature) => { const codeSignatureEntries: EntryNested[] = [];
return { const noDuplicates = new Map<string, boolean>();
subjectName: signature?.subject_name ?? '', return codeSignature.reduce((acc, signature) => {
trusted: signature?.trusted?.toString() ?? '', 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 { } else {
const signature: Flattened<CodeSignature> | undefined = !Array.isArray(codeSignature) const signature: Flattened<CodeSignature> | undefined = !Array.isArray(codeSignature)
? codeSignature ? codeSignature
: undefined; : undefined;
if (signature?.trusted === true) {
return [ return [
{ {
subjectName: signature?.subject_name ?? '', field,
trusted: signature?.trusted ?? '', 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. * Takes an array of Entries and filter out the ones with empty values.
* It will also filter out empty values for nested entries. * It will also filter out empty values for nested entries.
*/ */
function filterEmptyExceptionEntries<T extends ExceptionEntry>(entries: T[]): T[] {
const finalEntries: T[] = []; function filterEmptyExceptionEntries(entries: EntriesArray): EntriesArray {
const finalEntries: EntriesArray = [];
for (const entry of entries) { for (const entry of entries) {
if (entry.entries !== undefined) { if ('entries' in entry && entry.entries !== undefined) {
entry.entries = entry.entries.filter((el) => el.value !== undefined && el.value.length > 0); entry.entries = entry.entries.filter(
(el) => 'value' in el && el.value !== undefined && el.value.length > 0
);
finalEntries.push(entry); finalEntries.push(entry);
} else if (entry.value !== undefined && entry.value.length > 0) { } else if ('value' in entry && entry?.value?.length > 0) {
finalEntries.push(entry); finalEntries.push(entry);
} }
} }
return finalEntries; return finalEntries;
} }
@ -373,7 +452,6 @@ function filterEmptyExceptionEntries<T extends ExceptionEntry>(entries: T[]): T[
export const getPrepopulatedEndpointException = ({ export const getPrepopulatedEndpointException = ({
listId, listId,
name, name,
codeSignature,
eventCode, eventCode,
listNamespace = 'agnostic', listNamespace = 'agnostic',
alertEcsData, alertEcsData,
@ -381,21 +459,16 @@ export const getPrepopulatedEndpointException = ({
listId: string; listId: string;
listNamespace?: NamespaceType; listNamespace?: NamespaceType;
name: string; name: string;
codeSignature: { subjectName: string; trusted: string };
eventCode: string; eventCode: string;
alertEcsData: Flattened<Ecs>; alertEcsData: Flattened<Ecs>;
}): ExceptionsBuilderExceptionItem => { }): ExceptionsBuilderExceptionItem => {
const { file, host } = alertEcsData; const { file, host } = alertEcsData;
const fileCodeSignature = getFileCodeSignature(alertEcsData);
const filePath = file?.path ?? ''; const filePath = file?.path ?? '';
const sha256Hash = file?.hash?.sha256 ?? ''; const sha256Hash = file?.hash?.sha256 ?? '';
const isLinux = host?.os?.name === 'Linux'; const isLinux = host?.os?.name === 'Linux';
const commonFields: Array<{ const commonFields: EntriesArray = [
field: string;
operator: 'excluded' | 'included';
type: 'match';
value: string;
}> = [
{ {
field: isLinux ? 'file.path' : 'file.path.caseless', field: isLinux ? 'file.path' : 'file.path.caseless',
operator: 'included', operator: 'included',
@ -416,30 +489,10 @@ export const getPrepopulatedEndpointException = ({
}, },
]; ];
const entriesToAdd = () => { const entriesToAdd = () => {
if (isLinux) { if (!isLinux && fileCodeSignature !== undefined) {
return addIdToEntries(commonFields); return addIdToEntries(filterEmptyExceptionEntries(commonFields.concat(fileCodeSignature)));
} else { } else {
return addIdToEntries([ return addIdToEntries(filterEmptyExceptionEntries(commonFields));
{
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,
]);
} }
}; };
@ -455,7 +508,6 @@ export const getPrepopulatedEndpointException = ({
export const getPrepopulatedRansomwareException = ({ export const getPrepopulatedRansomwareException = ({
listId, listId,
name, name,
codeSignature,
eventCode, eventCode,
listNamespace = 'agnostic', listNamespace = 'agnostic',
alertEcsData, alertEcsData,
@ -463,35 +515,17 @@ export const getPrepopulatedRansomwareException = ({
listId: string; listId: string;
listNamespace?: NamespaceType; listNamespace?: NamespaceType;
name: string; name: string;
codeSignature: { subjectName: string; trusted: string };
eventCode: string; eventCode: string;
alertEcsData: Flattened<Ecs>; alertEcsData: Flattened<Ecs>;
}): ExceptionsBuilderExceptionItem => { }): ExceptionsBuilderExceptionItem => {
const { process, Ransomware } = alertEcsData; const { process, Ransomware, host } = alertEcsData;
const processCodeSignature = getProcessCodeSignature(alertEcsData);
const sha256Hash = process?.hash?.sha256 ?? ''; const sha256Hash = process?.hash?.sha256 ?? '';
const executable = process?.executable ?? ''; const executable = process?.executable ?? '';
const ransomwareFeature = Ransomware?.feature ?? ''; const ransomwareFeature = Ransomware?.feature ?? '';
return { const isLinux = host?.os?.name === 'Linux';
...getNewExceptionItem({ listId, namespaceType: listNamespace, name }),
entries: addIdToEntries([ const commonFields: EntriesArray = [
{
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', field: 'process.executable',
operator: 'included', operator: 'included',
@ -516,7 +550,19 @@ export const getPrepopulatedRansomwareException = ({
type: 'match', type: 'match',
value: eventCode ?? '', 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: entriesToAdd(),
}; };
}; };
@ -618,6 +664,7 @@ export const getPrepopulatedMemoryShellcodeException = ({
}; };
}; };
/* eslint complexity: ["error", 21]*/
export const getPrepopulatedBehaviorException = ({ export const getPrepopulatedBehaviorException = ({
listId, listId,
name, name,
@ -631,8 +678,11 @@ export const getPrepopulatedBehaviorException = ({
eventCode: string; eventCode: string;
alertEcsData: Flattened<Ecs>; alertEcsData: Flattened<Ecs>;
}): ExceptionsBuilderExceptionItem => { }): ExceptionsBuilderExceptionItem => {
const { process } = alertEcsData; const { process, host } = alertEcsData;
const entries = filterEmptyExceptionEntries([ const processCodeSignature = getProcessCodeSignature(alertEcsData);
const dllCodeSignature = getDllCodeSignature(alertEcsData);
const isLinux = host?.os?.name === 'Linux';
const commonFields: EntriesArray = [
{ {
field: 'rule.id', field: 'rule.id',
operator: 'included' as const, operator: 'included' as const,
@ -657,12 +707,6 @@ export const getPrepopulatedBehaviorException = ({
type: 'match' as const, type: 'match' as const,
value: process?.parent?.executable ?? '', 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', field: 'file.path',
operator: 'included' as const, operator: 'included' as const,
@ -711,12 +755,6 @@ export const getPrepopulatedBehaviorException = ({
type: 'match' as const, type: 'match' as const,
value: alertEcsData.dll?.path ?? '', 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', field: 'dll.pe.original_file_name',
operator: 'included' as const, operator: 'included' as const,
@ -741,10 +779,28 @@ export const getPrepopulatedBehaviorException = ({
type: 'match' as const, type: 'match' as const,
value: alertEcsData.user?.id ?? '', 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 { return {
...getNewExceptionItem({ listId, namespaceType: listNamespace, name }), ...getNewExceptionItem({ listId, namespaceType: listNamespace, name }),
entries: addIdToEntries(entries), entries: entriesToAdd(),
}; };
}; };
@ -757,7 +813,6 @@ export const defaultEndpointExceptionItems = (
alertEcsData: Flattened<Ecs> & { 'event.code'?: string } alertEcsData: Flattened<Ecs> & { 'event.code'?: string }
): ExceptionsBuilderExceptionItem[] => { ): ExceptionsBuilderExceptionItem[] => {
const eventCode = alertEcsData['event.code'] ?? alertEcsData.event?.code; const eventCode = alertEcsData['event.code'] ?? alertEcsData.event?.code;
switch (eventCode) { switch (eventCode) {
case 'behavior': case 'behavior':
return [ return [
@ -787,26 +842,24 @@ export const defaultEndpointExceptionItems = (
}), }),
]; ];
case 'ransomware': case 'ransomware':
return getProcessCodeSignature(alertEcsData).map((codeSignature) => return [
getPrepopulatedRansomwareException({ getPrepopulatedRansomwareException({
listId, listId,
name, name,
eventCode, eventCode,
codeSignature,
alertEcsData, alertEcsData,
}) }),
); ];
default: default:
// By default return the standard prepopulated Endpoint Exception fields // By default return the standard prepopulated Endpoint Exception fields
return getFileCodeSignature(alertEcsData).map((codeSignature) => return [
getPrepopulatedEndpointException({ getPrepopulatedEndpointException({
listId, listId,
name, name,
eventCode: eventCode ?? '', eventCode: eventCode ?? '',
codeSignature,
alertEcsData, alertEcsData,
}) }),
); ];
} }
}; };

View file

@ -23,7 +23,7 @@ export interface ExceptionsPagination {
export interface FlattenedCodeSignature { export interface FlattenedCodeSignature {
subject_name: string; subject_name: string;
trusted: string; trusted: boolean;
} }
export type Flattened<T> = { export type Flattened<T> = {