[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".
*/
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;

View file

@ -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[];

View file

@ -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',
},
},
];
};
}
/**

View file

@ -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<Ecs>
): 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<CodeSignature[] | CodeSignature> };
code_signature?: CodeSignature;
}
>(
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
* 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<Ecs>
): 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<Ecs>): 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<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
* a single object with subject_name and trusted.
*/
export const getCodeSignatureValue = (
codeSignature: Flattened<CodeSignature> | Flattened<CodeSignature[]> | undefined
): Array<{ subjectName: string; trusted: string }> => {
codeSignature: Flattened<CodeSignature> | 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<string, boolean>();
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<CodeSignature> | 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<T extends ExceptionEntry>(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<T extends ExceptionEntry>(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<Ecs>;
}): 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<Ecs>;
}): 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<Ecs>;
}): 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<Ecs> & { '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,
})
);
}),
];
}
};

View file

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