Skip to content

Commit

Permalink
[ResponseOps][Alerts] Fix Security cell value component props (#207095)
Browse files Browse the repository at this point in the history
## Summary

The Security Solution alerts table CellValue component was receiving
some wrong props when used in the Rule preview table. This PR removes
the spread `{...props}` expression and type cast that didn't catch this
error and correctly converts props and types.
  • Loading branch information
umbopepato committed Jan 23, 2025
1 parent 60043c8 commit ec8ab5b
Show file tree
Hide file tree
Showing 3 changed files with 192 additions and 154 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,47 @@
* 2.0.
*/

import type { ComponentProps } from 'react';
import React from 'react';
import type { EuiDataGridCellValueElementProps } from '@elastic/eui';
import { TableId } from '@kbn/securitysolution-data-table';
import type { LegacyField } from '@kbn/alerting-types';
import type { CellValueElementProps } from '../../../../../common/types';
import { SourcererScopeName } from '../../../../sourcerer/store/model';
import { CellValue } from '../../../../detections/configurations/security_solution_detections';

export const PreviewRenderCellValue: React.FC<
EuiDataGridCellValueElementProps & CellValueElementProps
> = (props) => {
> = ({
data,
ecsData,
setCellProps,
isExpandable,
isExpanded,
isDetails,
rowIndex,
colIndex,
columnId,
rowRenderers,
isDraggable,
truncate,
}) => {
return (
<CellValue
{...(props as unknown as ComponentProps<typeof CellValue>)}
asPlainText={true}
scopeId={SourcererScopeName.detections}
tableType={TableId.rulePreview}
scopeId={SourcererScopeName.detections}
legacyAlert={(data ?? []) as LegacyField[]}
ecsAlert={ecsData}
asPlainText={true}
setCellProps={setCellProps}
isExpandable={isExpandable}
isExpanded={isExpanded}
isDetails={isDetails}
rowIndex={rowIndex}
colIndex={colIndex}
columnId={columnId}
rowRenderers={rowRenderers}
isDraggable={isDraggable}
truncate={truncate}
/>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,9 @@ export type AlertTableContextMenuItem = EuiContextMenuPanelItemDescriptorEntry;

export interface SecurityAlertsTableContext {
tableType: TableId;
rowRenderers: RowRenderer[];
rowRenderers?: RowRenderer[];
isDetails: boolean;
truncate: boolean;
truncate?: boolean;
isDraggable: boolean;
leadingControlColumn: ControlColumnProps;
userProfiles: AlertsUserProfilesData;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
* 2.0.
*/

import React, { useMemo, memo, type ComponentProps } from 'react';
import { EuiIcon, EuiToolTip, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import React, { useMemo, memo } from 'react';
import { find, getOr } from 'lodash/fp';
import type { TimelineNonEcsData } from '@kbn/timelines-plugin/common';
import { tableDefaults, dataTableSelectors } from '@kbn/securitysolution-data-table';
Expand Down Expand Up @@ -35,157 +35,171 @@ import type { GetSecurityAlertsTableProp } from '../../components/alerts_table/t
* from the TGrid
*/

export const CellValue: GetSecurityAlertsTableProp<'renderCellValue'> = memo(
function RenderCellValue(props) {
const {
columnId,
rowIndex,
scopeId,
tableId,
tableType,
header,
legacyAlert,
ecsAlert,
linkValues,
rowRenderers,
isDetails,
isExpandable,
isDraggable = false,
isExpanded,
colIndex,
eventId,
setCellProps,
truncate,
context,
} = props;
const isTourAnchor = useMemo(
() =>
columnId === SIGNAL_RULE_NAME_FIELD_NAME &&
isDetectionsAlertsTable(tableType) &&
rowIndex === 0 &&
!props.isDetails,
[columnId, props.isDetails, rowIndex, tableType]
);
const { browserFields } = useSourcererDataView(scopeId);
const browserFieldsByName = useMemo(() => getAllFieldsByName(browserFields), [browserFields]);
const getTable = useMemo(() => dataTableSelectors.getTableByIdSelector(), []);
const license = useLicense();
const viewMode =
useDeepEqualSelector((state) => (getTable(state, tableId ?? '') ?? tableDefaults).viewMode) ??
tableDefaults.viewMode;
type RenderCellValueProps = Pick<
ComponentProps<GetSecurityAlertsTableProp<'renderCellValue'>>,
| 'columnId'
| 'rowIndex'
| 'tableId'
| 'tableType'
| 'legacyAlert'
| 'ecsAlert'
| 'rowRenderers'
| 'isDetails'
| 'isExpandable'
| 'isDraggable'
| 'isExpanded'
| 'colIndex'
| 'setCellProps'
| 'truncate'
> &
Record<string, unknown>;

const gridColumns = useMemo(() => {
return getColumns(license);
}, [license]);
export const CellValue = memo(function RenderCellValue({
columnId,
rowIndex,
scopeId,
tableId,
tableType,
header,
legacyAlert,
ecsAlert,
linkValues,
rowRenderers,
isDetails,
isExpandable,
isDraggable = false,
isExpanded,
colIndex,
eventId,
setCellProps,
truncate,
context,
}: RenderCellValueProps) {
const isTourAnchor = useMemo(
() =>
columnId === SIGNAL_RULE_NAME_FIELD_NAME &&
isDetectionsAlertsTable(tableType) &&
rowIndex === 0 &&
!isDetails,
[columnId, isDetails, rowIndex, tableType]
);
const { browserFields } = useSourcererDataView(scopeId);
const browserFieldsByName = useMemo(() => getAllFieldsByName(browserFields), [browserFields]);
const getTable = useMemo(() => dataTableSelectors.getTableByIdSelector(), []);
const license = useLicense();
const viewMode =
useDeepEqualSelector((state) => (getTable(state, tableId ?? '') ?? tableDefaults).viewMode) ??
tableDefaults.viewMode;

const columnHeaders = useMemo(() => {
return viewMode === VIEW_SELECTION.gridView ? gridColumns : eventRenderedViewColumns;
}, [gridColumns, viewMode]);
const gridColumns = useMemo(() => {
return getColumns(license);
}, [license]);

/**
* There is difference between how `triggers actions` fetched data v/s
* how security solution fetches data via timelineSearchStrategy
*
* _id and _index fields are array in timelineSearchStrategy but not in
* ruleStrategy
*
*
*/
const columnHeaders = useMemo(() => {
return viewMode === VIEW_SELECTION.gridView ? gridColumns : eventRenderedViewColumns;
}, [gridColumns, viewMode]);

const finalData = useMemo(() => {
return (legacyAlert as TimelineNonEcsData[]).map((field) => {
if (['_id', '_index'].includes(field.field)) {
const newValue = field.value ?? '';
return {
field: field.field,
value: Array.isArray(newValue) ? newValue : [newValue],
};
} else {
return field;
}
});
}, [legacyAlert]);
/**
* There is difference between how `triggers actions` fetched data v/s
* how security solution fetches data via timelineSearchStrategy
*
* _id and _index fields are array in timelineSearchStrategy but not in
* ruleStrategy
*
*
*/

const actualSuppressionCount = useMemo(() => {
// We check both ecsAlert and data for the suppression count because it could be in either one,
// depending on where RenderCellValue is being used - when used in cases, data is populated,
// whereas in the regular security alerts table it's in ecsAlert
const ecsSuppressionCount = ecsAlert?.kibana?.alert.suppression?.docs_count?.[0];
const dataSuppressionCount = find(
{ field: 'kibana.alert.suppression.docs_count' },
legacyAlert
)?.value?.[0] as number | undefined;
return ecsSuppressionCount ? parseInt(ecsSuppressionCount, 10) : dataSuppressionCount;
}, [ecsAlert, legacyAlert]);
const finalData = useMemo(() => {
return (legacyAlert as TimelineNonEcsData[]).map((field) => {
if (['_id', '_index'].includes(field.field)) {
const newValue = field.value ?? '';
return {
field: field.field,
value: Array.isArray(newValue) ? newValue : [newValue],
};
} else {
return field;
}
});
}, [legacyAlert]);

const Renderer = useMemo(() => {
const myHeader = header ?? { id: columnId, ...browserFieldsByName[columnId] };
const colHeader = columnHeaders.find((col) => col.id === columnId);
const localLinkValues = getOr([], colHeader?.linkField ?? '', ecsAlert);
return (
<GuidedOnboardingTourStep
isTourAnchor={isTourAnchor}
step={AlertsCasesTourSteps.pointToAlertName}
tourId={SecurityStepId.alertsCases}
>
<DefaultCellRenderer
browserFields={browserFields}
columnId={columnId}
data={finalData}
ecsData={ecsAlert}
eventId={eventId}
header={myHeader}
isDetails={isDetails}
isDraggable={isDraggable}
isExpandable={isExpandable}
isExpanded={isExpanded}
linkValues={linkValues ?? localLinkValues}
rowIndex={rowIndex}
colIndex={colIndex}
rowRenderers={rowRenderers ?? defaultRowRenderers}
setCellProps={setCellProps}
scopeId={scopeId}
truncate={truncate}
asPlainText={false}
context={context}
/>
</GuidedOnboardingTourStep>
);
}, [
header,
columnId,
browserFieldsByName,
columnHeaders,
ecsAlert,
isTourAnchor,
browserFields,
finalData,
eventId,
isDetails,
isDraggable,
isExpandable,
isExpanded,
linkValues,
rowIndex,
colIndex,
rowRenderers,
setCellProps,
scopeId,
truncate,
context,
]);
const actualSuppressionCount = useMemo(() => {
// We check both ecsAlert and data for the suppression count because it could be in either one,
// depending on where RenderCellValue is being used - when used in cases, data is populated,
// whereas in the regular security alerts table it's in ecsAlert
const ecsSuppressionCount = ecsAlert?.kibana?.alert.suppression?.docs_count?.[0];
const dataSuppressionCount = find({ field: 'kibana.alert.suppression.docs_count' }, legacyAlert)
?.value?.[0] as number | undefined;
return ecsSuppressionCount ? parseInt(ecsSuppressionCount, 10) : dataSuppressionCount;
}, [ecsAlert, legacyAlert]);

return columnId === SIGNAL_RULE_NAME_FIELD_NAME && actualSuppressionCount ? (
<EuiFlexGroup gutterSize="xs">
<EuiFlexItem grow={false}>
<EuiToolTip position="top" content={SUPPRESSED_ALERT_TOOLTIP(actualSuppressionCount)}>
<EuiIcon type="layers" />
</EuiToolTip>
</EuiFlexItem>
<EuiFlexItem grow={false}>{Renderer}</EuiFlexItem>
</EuiFlexGroup>
) : (
<>{Renderer}</>
const Renderer = useMemo(() => {
const myHeader = header ?? { id: columnId, ...browserFieldsByName[columnId] };
const colHeader = columnHeaders.find((col) => col.id === columnId);
const localLinkValues = getOr([], colHeader?.linkField ?? '', ecsAlert);
return (
<GuidedOnboardingTourStep
isTourAnchor={isTourAnchor}
step={AlertsCasesTourSteps.pointToAlertName}
tourId={SecurityStepId.alertsCases}
>
<DefaultCellRenderer
browserFields={browserFields}
columnId={columnId}
data={finalData}
ecsData={ecsAlert}
eventId={eventId}
header={myHeader}
isDetails={isDetails}
isDraggable={isDraggable}
isExpandable={isExpandable}
isExpanded={isExpanded}
linkValues={linkValues ?? localLinkValues}
rowIndex={rowIndex}
colIndex={colIndex}
rowRenderers={rowRenderers ?? defaultRowRenderers}
setCellProps={setCellProps}
scopeId={scopeId}
truncate={truncate}
asPlainText={false}
context={context}
/>
</GuidedOnboardingTourStep>
);
}
);
}, [
header,
columnId,
browserFieldsByName,
columnHeaders,
ecsAlert,
isTourAnchor,
browserFields,
finalData,
eventId,
isDetails,
isDraggable,
isExpandable,
isExpanded,
linkValues,
rowIndex,
colIndex,
rowRenderers,
setCellProps,
scopeId,
truncate,
context,
]);

return columnId === SIGNAL_RULE_NAME_FIELD_NAME && actualSuppressionCount ? (
<EuiFlexGroup gutterSize="xs">
<EuiFlexItem grow={false}>
<EuiToolTip position="top" content={SUPPRESSED_ALERT_TOOLTIP(actualSuppressionCount)}>
<EuiIcon type="layers" />
</EuiToolTip>
</EuiFlexItem>
<EuiFlexItem grow={false}>{Renderer}</EuiFlexItem>
</EuiFlexGroup>
) : (
<>{Renderer}</>
);
});

0 comments on commit ec8ab5b

Please sign in to comment.