Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions x-pack/legacy/plugins/siem/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export const DETECTION_ENGINE_PREPACKAGED_URL = `${DETECTION_ENGINE_RULES_URL}/p
export const DETECTION_ENGINE_PRIVILEGES_URL = `${DETECTION_ENGINE_URL}/privileges`;
export const DETECTION_ENGINE_INDEX_URL = `${DETECTION_ENGINE_URL}/index`;
export const DETECTION_ENGINE_TAGS_URL = `${DETECTION_ENGINE_URL}/tags`;
export const DETECTION_ENGINE_RULES_STATUS = `${DETECTION_ENGINE_URL}/rules/_find_statuses`;

/**
* Default signals index key for kibana.dev.yml
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,14 @@ import {
ImportRulesProps,
ExportRulesProps,
RuleError,
RuleStatus,
ImportRulesResponse,
} from './types';
import { throwIfNotOk } from '../../../hooks/api/api';
import {
DETECTION_ENGINE_RULES_URL,
DETECTION_ENGINE_PREPACKAGED_URL,
DETECTION_ENGINE_RULES_STATUS,
} from '../../../../common/constants';
import * as i18n from '../../../pages/detection_engine/rules/translations';

Expand Down Expand Up @@ -302,3 +304,36 @@ export const exportRules = async ({
await throwIfNotOk(response);
return response.blob();
};

/**
* Get Rule Status provided Rule ID
*
* @param id string of Rule ID's (not rule_id)
*
* @throws An error if response is not OK
*/
export const getRuleStatusById = async ({
id,
signal,
}: {
id: string;
signal: AbortSignal;
}): Promise<Record<string, RuleStatus[]>> => {
const response = await fetch(
`${chrome.getBasePath()}${DETECTION_ENGINE_RULES_STATUS}?ids=${encodeURIComponent(
JSON.stringify([id])
)}`,
{
method: 'GET',
credentials: 'same-origin',
headers: {
'content-type': 'application/json',
'kbn-xsrf': 'true',
},
signal,
}
);

await throwIfNotOk(response);
return response.json();
};
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,12 @@ export const RuleSchema = t.intersection([
updated_by: t.string,
}),
t.partial({
last_failure_at: t.string,
last_failure_message: t.string,
output_index: t.string,
saved_id: t.string,
status: t.string,
status_date: t.string,
timeline_id: t.string,
timeline_title: t.string,
version: t.number,
Expand Down Expand Up @@ -175,3 +179,13 @@ export interface ExportRulesProps {
excludeExportDetails?: boolean;
signal: AbortSignal;
}

export interface RuleStatus {
alert_id: string;
status_date: string;
status: string;
last_failure_at: string | null;
last_success_at: string | null;
last_failure_message: string | null;
last_success_message: string | null;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { useEffect, useState } from 'react';

import { useStateToaster } from '../../../components/toasters';
import { errorToToaster } from '../../../components/ml/api/error_to_toaster';
import { getRuleStatusById } from './api';
import * as i18n from './translations';
import { RuleStatus } from './types';

type Return = [boolean, RuleStatus[] | null];

/**
* Hook for using to get a Rule from the Detection Engine API
*
* @param id desired Rule ID's (not rule_id)
*
*/
export const useRuleStatus = (id: string | undefined | null): Return => {
const [ruleStatus, setRuleStatus] = useState<RuleStatus[] | null>(null);
const [loading, setLoading] = useState(true);
const [, dispatchToaster] = useStateToaster();

useEffect(() => {
let isSubscribed = true;
const abortCtrl = new AbortController();

async function fetchData(idToFetch: string) {
try {
setLoading(true);
const ruleStatusResponse = await getRuleStatusById({
id: idToFetch,
signal: abortCtrl.signal,
});

if (isSubscribed) {
setRuleStatus(ruleStatusResponse[id ?? '']);
}
} catch (error) {
if (isSubscribed) {
setRuleStatus(null);
errorToToaster({ title: i18n.RULE_FETCH_FAILURE, error, dispatchToaster });
}
}
if (isSubscribed) {
setLoading(false);
}
}
if (id != null) {
fetchData(id);
}
return () => {
isSubscribed = false;
abortCtrl.abort();
};
}, [id]);

return [loading, ruleStatus];
};
10 changes: 6 additions & 4 deletions x-pack/legacy/plugins/siem/public/containers/timeline/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { getOr } from 'lodash/fp';
import { isEmpty, getOr } from 'lodash/fp';
import memoizeOne from 'memoize-one';
import React from 'react';
import { Query } from 'react-apollo';
Expand Down Expand Up @@ -79,15 +79,17 @@ class TimelineQueryComponent extends QueryTemplate<
sourceId,
sortField,
} = this.props;
const defaultIndex =
indexPattern == null || isEmpty(indexPattern)
? kibana.services.uiSettings.get<string[]>(DEFAULT_INDEX_KEY)
: indexPattern?.title.split(',');
const variables: GetTimelineQuery.Variables = {
fieldRequested: fields,
filterQuery: createFilter(filterQuery),
sourceId,
pagination: { limit, cursor: null, tiebreaker: null },
sortField,
defaultIndex:
indexPattern?.title.split(',') ??
kibana.services.uiSettings.get<string[]>(DEFAULT_INDEX_KEY),
defaultIndex,
inspect: isInspected,
};
return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*/

import { EuiPanel, EuiLoadingContent } from '@elastic/eui';
import { isEmpty } from 'lodash/fp';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { connect } from 'react-redux';
import { ActionCreator } from 'typescript-fsa';
Expand Down Expand Up @@ -299,7 +300,7 @@ export const SignalsTableComponent = React.memo<SignalsTableComponentProps>(
[additionalActions, canUserCRUD, selectAll]
);

if (loading) {
if (loading || isEmpty(signalsIndex)) {
return (
<EuiPanel>
<HeaderSection title={i18n.SIGNALS_TABLE_TITLE} />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

/* eslint-disable react/display-name */

import {
EuiBasicTable,
EuiPanel,
EuiLoadingContent,
EuiHealth,
EuiBasicTableColumn,
} from '@elastic/eui';
import React, { memo } from 'react';

import { useRuleStatus } from '../../../../containers/detection_engine/rules/use_rule_status';
import { RuleStatus } from '../../../../containers/detection_engine/rules';
import { HeaderSection } from '../../../../components/header_section';
import * as i18n from './translations';
import { FormattedDate } from '../../../../components/formatted_date';

interface FailureHistoryProps {
id?: string | null;
}

const FailureHistoryComponent: React.FC<FailureHistoryProps> = ({ id }) => {
const [loading, ruleStatus] = useRuleStatus(id);
if (loading) {
return (
<EuiPanel>
<HeaderSection title={i18n.LAST_FIVE_ERRORS} />
<EuiLoadingContent />
</EuiPanel>
);
}
const columns: Array<EuiBasicTableColumn<RuleStatus>> = [
{
name: i18n.COLUMN_STATUS_TYPE,
render: () => <EuiHealth color="danger">{i18n.TYPE_FAILED}</EuiHealth>,
truncateText: false,
width: '16%',
},
{
field: 'last_failure_at',
name: i18n.COLUMN_FAILED_AT,
render: (value: string) => <FormattedDate value={value} fieldName="last_failure_at" />,
sortable: false,
truncateText: false,
width: '24%',
},
{
field: 'last_failure_message',
name: i18n.COLUMN_FAILED_MSG,
render: (value: string) => <>{value}</>,
sortable: false,
truncateText: false,
width: '60%',
},
];
return (
<EuiPanel>
<HeaderSection title={i18n.LAST_FIVE_ERRORS} />
<EuiBasicTable
columns={columns}
loading={loading}
items={ruleStatus != null ? ruleStatus?.filter(rs => rs.last_failure_at != null) : []}
sorting={{ sort: { field: 'status_date', direction: 'desc' } }}
/>
</EuiPanel>
);
};

export const FailureHistory = memo(FailureHistoryComponent);
Loading