/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/
import React, { Component } from 'react';
import { RouteComponentProps } from 'react-router-dom';
import moment from 'moment';
import {
EuiBasicTableColumn,
EuiButtonIcon,
EuiInMemoryTable,
EuiLink,
EuiToolTip,
EuiEmptyPrompt,
EuiBadge,
} from '@elastic/eui';
import { FieldValueSelectionFilterConfigType } from '@elastic/eui/src/components/search_bar/filters/field_value_selection_filter';
import dateMath from '@elastic/datemath';
import { capitalizeFirstLetter, formatRuleType, renderTime } from '../../../../utils/helpers';
import { DEFAULT_EMPTY_DATA } from '../../../../utils/constants';
import {
DetectorsService,
OpenSearchService,
IndexPatternsService,
CorrelationService,
} from '../../../../services';
import { Finding } from '../../models/interfaces';
import CreateAlertFlyout from '../CreateAlertFlyout';
import { NotificationChannelTypeOptions } from '../../../CreateDetector/components/ConfigureAlerts/models/interfaces';
import { FindingItemType } from '../../containers/Findings/Findings';
import { parseAlertSeverityToOption } from '../../../CreateDetector/components/ConfigureAlerts/utils/helpers';
import { RuleSource } from '../../../../../server/models/interfaces';
import { DataStore } from '../../../../store/DataStore';
import { getSeverityColor } from '../../../Correlations/utils/constants';
interface FindingsTableProps extends RouteComponentProps {
detectorService: DetectorsService;
opensearchService: OpenSearchService;
findings: FindingItemType[];
notificationChannels: NotificationChannelTypeOptions[];
refreshNotificationChannels: () => void;
loading: boolean;
rules: { [id: string]: RuleSource };
startTime: string;
endTime: string;
onRefresh: () => void;
onFindingsFiltered: (findings: FindingItemType[]) => void;
hasNotificationsPlugin: boolean;
indexPatternsService: IndexPatternsService;
correlationService: CorrelationService;
}
interface FindingsTableState {
findingsFiltered: boolean;
filteredFindings: FindingItemType[];
flyout: object | undefined;
flyoutOpen: boolean;
selectedFinding?: Finding;
widgetEmptyMessage: React.ReactNode | undefined;
}
export default class FindingsTable extends Component {
constructor(props: FindingsTableProps) {
super(props);
this.state = {
findingsFiltered: false,
filteredFindings: [],
flyout: undefined,
flyoutOpen: false,
selectedFinding: undefined,
widgetEmptyMessage: undefined,
};
}
componentDidMount = async () => {
await this.filterFindings();
};
componentDidUpdate(prevProps: Readonly) {
if (
prevProps.startTime !== this.props.startTime ||
prevProps.endTime !== this.props.endTime ||
prevProps.findings.length !== this.props.findings.length
)
this.filterFindings();
}
filterFindings = () => {
const { findings, startTime, endTime } = this.props;
const startMoment = dateMath.parse(startTime);
const endMoment = dateMath.parse(endTime);
const filteredFindings = findings.filter((finding) =>
moment(finding.timestamp).isBetween(moment(startMoment), moment(endMoment))
);
this.setState({
findingsFiltered: true,
filteredFindings: filteredFindings,
widgetEmptyMessage:
filteredFindings.length || findings.length ? undefined : (
No findings.Adjust the time range to see
more results.
}
/>
),
});
this.props.onFindingsFiltered(filteredFindings);
};
closeFlyout = (refreshPage: boolean = false) => {
this.setState({ flyout: undefined, flyoutOpen: false, selectedFinding: undefined });
if (refreshPage) this.props.onRefresh();
};
renderCreateAlertFlyout = (finding: Finding) => {
if (this.state.flyoutOpen) this.closeFlyout();
else {
const ruleOptions = finding.queries.map((query) => {
const rule = this.props.rules[query.id];
return {
name: rule.title,
id: query.id,
severity: rule.level,
tags: rule.tags.map((tag: any) => tag.value),
};
});
this.setState({
flyout: (
),
flyoutOpen: true,
selectedFinding: finding,
});
}
};
render() {
const { findings, loading, rules } = this.props;
const {
findingsFiltered,
filteredFindings,
flyout,
flyoutOpen,
widgetEmptyMessage,
} = this.state;
const columns: EuiBasicTableColumn[] = [
{
field: 'timestamp',
name: 'Time',
sortable: true,
dataType: 'date',
render: renderTime,
},
{
field: 'id',
name: 'Finding ID',
sortable: true,
dataType: 'string',
render: (id, finding) =>
(
DataStore.findings.openFlyout(finding, this.state.filteredFindings)}
data-test-subj={'finding-details-flyout-button'}
>
{`${(id as string).slice(0, 7)}...`}
) || DEFAULT_EMPTY_DATA,
},
{
field: 'ruleName',
name: 'Rule name',
sortable: true,
dataType: 'string',
render: (ruleName: string) => ruleName || DEFAULT_EMPTY_DATA,
},
{
field: 'detectorName',
name: 'Threat detector',
sortable: true,
dataType: 'string',
render: (name: string) => name || DEFAULT_EMPTY_DATA,
},
{
field: 'logType',
name: 'Log type',
sortable: true,
dataType: 'string',
render: (logType: string) => formatRuleType(logType),
},
{
field: 'ruleSeverity',
name: 'Rule severity',
sortable: true,
dataType: 'string',
align: 'left',
render: (ruleSeverity: string) => {
const severity = capitalizeFirstLetter(ruleSeverity) || DEFAULT_EMPTY_DATA;
const { background, text } = getSeverityColor(severity);
return (
{severity}
);
},
},
{
name: 'Actions',
sortable: false,
actions: [
{
render: (finding) => (
DataStore.findings.openFlyout(finding, this.state.filteredFindings)
}
/>
),
},
{
render: (finding) => (
this.renderCreateAlertFlyout(finding)}
/>
),
},
],
},
];
const logTypes = new Set();
const severities = new Set();
filteredFindings.forEach((finding) => {
if (finding) {
const queryId = finding.queries[0].id;
logTypes.add(rules[queryId].category);
severities.add(rules[queryId].level);
}
});
const search = {
box: {
placeholder: 'Search findings',
schema: true,
},
filters: [
{
type: 'field_value_selection',
field: 'ruleSeverity',
name: 'Rule severity',
options: Array.from(severities).map((severity) => {
const name =
parseAlertSeverityToOption(severity)?.label || capitalizeFirstLetter(severity);
return { value: severity, name: name || severity };
}),
multiSelect: 'or',
} as FieldValueSelectionFilterConfigType,
{
type: 'field_value_selection',
field: 'logType',
name: 'Log type',
options: Array.from(logTypes).map((type) => ({
value: type,
name: formatRuleType(type),
})),
multiSelect: 'or',
} as FieldValueSelectionFilterConfigType,
],
};
const sorting: any = {
sort: {
field: 'timestamp',
direction: 'desc',
},
};
return (
item.id}
pagination={true}
search={search}
sorting={sorting}
isSelectable={false}
loading={loading}
message={widgetEmptyMessage}
/>
{flyoutOpen && flyout}
);
}
}