mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 01:13:23 -04:00
[Security Solution][Analyzer] Add alerts to analyzer, display alerts by process ancestry in alert flyout (#135340)
* WIP stats appearing in tree api, events api TODO * All panels work, types/tests TODO * WIP handle events with only alerts or only events better * Throw away commit just POC alert ids in tree response * Remove console.log * [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' * Fix some tests and 2/3 type errors, still WIP * Disable tree request until entity request succeeds * Remove console.log * Fix remaining types * Create shared hook for timeline selectors used by analyzer * Remove reset scroll * Change type definition for getRacClient * Address pr comments Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
3515e6f22c
commit
3669e79b82
28 changed files with 542 additions and 235 deletions
|
@ -191,9 +191,11 @@ export function processNameSafeVersion(event: SafeResolverEvent): string | undef
|
|||
}
|
||||
|
||||
export function eventID(event: SafeResolverEvent): number | undefined | string {
|
||||
return firstNonNullValue(
|
||||
isLegacyEventSafeVersion(event) ? event.endgame.serial_event_id : event.event?.id
|
||||
);
|
||||
if (isLegacyEventSafeVersion(event)) {
|
||||
return firstNonNullValue(event.endgame?.serial_event_id);
|
||||
} else {
|
||||
return firstNonNullValue(event.event?.id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -217,11 +219,11 @@ export function eventIDSafeVersion(event: SafeResolverEvent): number | undefined
|
|||
/**
|
||||
* The event.entity_id field.
|
||||
*/
|
||||
export function entityId(event: ResolverEvent): string {
|
||||
if (isLegacyEvent(event)) {
|
||||
export function entityId(event: SafeResolverEvent): string | undefined {
|
||||
if (isLegacyEventSafeVersion(event)) {
|
||||
return event.endgame.unique_pid ? String(event.endgame.unique_pid) : '';
|
||||
}
|
||||
return event.process.entity_id;
|
||||
return firstNonNullValue(event.process?.entity_id);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -42,6 +42,7 @@ export const validateTree = {
|
|||
minSize: 1,
|
||||
}),
|
||||
indexPatterns: schema.arrayOf(schema.string(), { minSize: 1 }),
|
||||
includeHits: schema.boolean({ defaultValue: false }),
|
||||
}),
|
||||
};
|
||||
|
||||
|
@ -61,6 +62,8 @@ export const validateEvents = {
|
|||
}),
|
||||
indexPatterns: schema.arrayOf(schema.string()),
|
||||
filter: schema.maybe(schema.string()),
|
||||
entityType: schema.maybe(schema.string()),
|
||||
eventID: schema.maybe(schema.string()),
|
||||
}),
|
||||
};
|
||||
|
||||
|
|
|
@ -699,6 +699,13 @@ export type SafeEndpointEvent = Partial<{
|
|||
kind: ECSField<string>;
|
||||
sequence: ECSField<number>;
|
||||
}>;
|
||||
kibana: Partial<{
|
||||
alert: Partial<{
|
||||
rule: Partial<{
|
||||
name: ECSField<string>;
|
||||
}>;
|
||||
}>;
|
||||
}>;
|
||||
host: Partial<{
|
||||
id: ECSField<string>;
|
||||
hostname: ECSField<string>;
|
||||
|
|
|
@ -79,6 +79,7 @@ export const RelatedAlertsByProcessAncestry = React.memo<Props>(({ data, eventId
|
|||
return (
|
||||
<FetchAndNotifyCachedAlertsByProcessAncestry
|
||||
data={data}
|
||||
eventId={eventId}
|
||||
timelineId={timelineId}
|
||||
onCacheLoad={setCache}
|
||||
/>
|
||||
|
@ -110,14 +111,11 @@ RelatedAlertsByProcessAncestry.displayName = 'RelatedAlertsByProcessAncestry';
|
|||
*/
|
||||
const FetchAndNotifyCachedAlertsByProcessAncestry: React.FC<{
|
||||
data: TimelineEventsDetailsItem;
|
||||
eventId: string;
|
||||
timelineId?: string;
|
||||
onCacheLoad: (cache: Cache) => void;
|
||||
}> = ({ data, timelineId, onCacheLoad }) => {
|
||||
const { loading, error, alertIds } = useAlertPrevalenceFromProcessTree({
|
||||
parentEntityId: data.values,
|
||||
timelineId: timelineId ?? '',
|
||||
signalIndexName: null,
|
||||
});
|
||||
}> = ({ data, timelineId, onCacheLoad, eventId }) => {
|
||||
const { loading, error, alertIds } = useAlertPrevalenceFromProcessTree(eventId, timelineId);
|
||||
|
||||
useEffect(() => {
|
||||
if (alertIds) {
|
||||
|
|
|
@ -4,27 +4,12 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { useQuery } from 'react-query';
|
||||
import { useHttp } from '../../lib/kibana';
|
||||
|
||||
// import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../../common/constants';
|
||||
|
||||
// import { useGlobalTime } from '../use_global_time';
|
||||
// import { TimelineId } from '../../../../common/types';
|
||||
// import { useDeepEqualSelector } from '../../hooks/use_selector';
|
||||
// import { inputsSelectors } from '../../store';
|
||||
import { useTimelineDataFilters } from '../../../timelines/containers/use_timeline_data_filters';
|
||||
|
||||
export const DETECTIONS_ALERTS_COUNT_ID = 'detections-alerts-count';
|
||||
|
||||
interface UseAlertPrevalenceOptions {
|
||||
parentEntityId: string | string[] | undefined | null;
|
||||
timelineId: string;
|
||||
signalIndexName: string | null;
|
||||
}
|
||||
|
||||
interface UserAlertPrevalenceFromProcessTreeResult {
|
||||
loading: boolean;
|
||||
alertIds: undefined | string[];
|
||||
|
@ -36,20 +21,91 @@ interface ProcessTreeAlertPrevalenceResponse {
|
|||
alertIds: string[];
|
||||
}
|
||||
|
||||
export function useAlertPrevalenceFromProcessTreeActual(
|
||||
processEntityId: string
|
||||
interface EntityResponse {
|
||||
id: string;
|
||||
name: string;
|
||||
schema: object;
|
||||
}
|
||||
|
||||
interface TreeResponse {
|
||||
statsNodes: Array<{
|
||||
data: object;
|
||||
id: string;
|
||||
parent: string;
|
||||
stats: {
|
||||
total: number;
|
||||
byCategory: {
|
||||
alerts?: number;
|
||||
};
|
||||
};
|
||||
}>;
|
||||
alertIds: string[];
|
||||
}
|
||||
|
||||
function useAlertDocumentAnalyzerSchema(processEntityId: string, indices: string[]) {
|
||||
const http = useHttp();
|
||||
const query = useQuery<EntityResponse[]>(['getAlertPrevalenceSchema', processEntityId], () => {
|
||||
return http.get<EntityResponse[]>(`/api/endpoint/resolver/entity`, {
|
||||
query: {
|
||||
_id: processEntityId,
|
||||
indices,
|
||||
},
|
||||
});
|
||||
});
|
||||
if (query.isLoading) {
|
||||
return {
|
||||
loading: true,
|
||||
error: false,
|
||||
id: null,
|
||||
schema: null,
|
||||
};
|
||||
} else if (query.data) {
|
||||
const {
|
||||
data: [{ schema, id }],
|
||||
} = query;
|
||||
return {
|
||||
loading: false,
|
||||
error: false,
|
||||
id,
|
||||
schema,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
loading: false,
|
||||
error: true,
|
||||
id: null,
|
||||
schema: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function useAlertPrevalenceFromProcessTree(
|
||||
processEntityId: string,
|
||||
timelineId: string | undefined
|
||||
): UserAlertPrevalenceFromProcessTreeResult {
|
||||
const http = useHttp();
|
||||
const query = useQuery<ProcessTreeAlertPrevalenceResponse>(
|
||||
['getAlertPrevalenceFromProcessTree', processEntityId],
|
||||
() => {
|
||||
return http.get<ProcessTreeAlertPrevalenceResponse>('/TBD', {
|
||||
query: { processEntityId },
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
if (query.isLoading) {
|
||||
const { selectedPatterns, to, from } = useTimelineDataFilters(timelineId);
|
||||
|
||||
const { loading, id, schema } = useAlertDocumentAnalyzerSchema(processEntityId, selectedPatterns);
|
||||
const query = useQuery<ProcessTreeAlertPrevalenceResponse>(
|
||||
['getAlertPrevalenceFromProcessTree', id],
|
||||
() => {
|
||||
return http.post<TreeResponse>(`/api/endpoint/resolver/tree`, {
|
||||
body: JSON.stringify({
|
||||
schema,
|
||||
ancestors: 200,
|
||||
descendants: 500,
|
||||
indexPatterns: selectedPatterns,
|
||||
nodes: [id],
|
||||
timeRange: { from, to },
|
||||
includeHits: true,
|
||||
}),
|
||||
});
|
||||
},
|
||||
{ enabled: schema !== null && id !== null }
|
||||
);
|
||||
if (query.isLoading || loading) {
|
||||
return {
|
||||
loading: true,
|
||||
error: false,
|
||||
|
@ -69,42 +125,3 @@ export function useAlertPrevalenceFromProcessTreeActual(
|
|||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const useAlertPrevalenceFromProcessTree = ({
|
||||
parentEntityId,
|
||||
timelineId,
|
||||
signalIndexName,
|
||||
}: UseAlertPrevalenceOptions): UserAlertPrevalenceFromProcessTreeResult => {
|
||||
// const timelineTime = useDeepEqualSelector((state) =>
|
||||
// inputsSelectors.timelineTimeRangeSelector(state)
|
||||
// );
|
||||
// const globalTime = useGlobalTime();
|
||||
|
||||
// const { to, from } = timelineId === TimelineId.active ? timelineTime : globalTime;
|
||||
const [{ loading, alertIds }, setResult] = useState<{ loading: boolean; alertIds?: string[] }>({
|
||||
loading: true,
|
||||
alertIds: undefined,
|
||||
});
|
||||
useEffect(() => {
|
||||
const t = setTimeout(() => {
|
||||
setResult({
|
||||
loading: false,
|
||||
alertIds: [
|
||||
'489ef2e50e7bb6366c5eaa1b17873e56fda738134685ca54b997a2546834f08c',
|
||||
'4b8e7111166034f94f62a009fa22ad42bfbb8edc86cda03055d14a9f2dd21f48',
|
||||
'0347030aa3593566a7fcd77769c798efaf02f84a3196fd586b4700c0c9ae5872',
|
||||
],
|
||||
});
|
||||
}, Math.random() * 1500 + 500);
|
||||
return () => {
|
||||
clearTimeout(t);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
loading,
|
||||
alertIds,
|
||||
count: alertIds ? alertIds.length : undefined,
|
||||
error: false,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -13,21 +13,6 @@ import { useShallowEqualSelector } from '../../hooks/use_selector';
|
|||
import { inputsSelectors } from '../../store';
|
||||
import { inputsActions } from '../../store/actions';
|
||||
|
||||
export const resetScroll = () => {
|
||||
setTimeout(() => {
|
||||
window.scrollTo(0, 0);
|
||||
|
||||
const kibanaBody = document.querySelector('#kibana-body');
|
||||
if (kibanaBody != null) {
|
||||
kibanaBody.scrollTop = 0;
|
||||
}
|
||||
|
||||
const pageContainer = document.querySelector('[data-test-subj="pageContainer"]');
|
||||
if (pageContainer != null) {
|
||||
pageContainer.scrollTop = 0;
|
||||
}
|
||||
}, 0);
|
||||
};
|
||||
export interface GlobalFullScreen {
|
||||
globalFullScreen: boolean;
|
||||
setGlobalFullScreen: (fullScreen: boolean) => void;
|
||||
|
@ -47,10 +32,8 @@ export const useGlobalFullScreen = (): GlobalFullScreen => {
|
|||
const isDataGridFullScreen = document.querySelector('.euiDataGrid--fullScreen') !== null;
|
||||
if (fullScreen) {
|
||||
document.body.classList.add(SCROLLING_DISABLED_CLASS_NAME, 'euiDataGrid__restrictBody');
|
||||
resetScroll();
|
||||
} else if (isDataGridFullScreen === false || fullScreen === false) {
|
||||
document.body.classList.remove(SCROLLING_DISABLED_CLASS_NAME, 'euiDataGrid__restrictBody');
|
||||
resetScroll();
|
||||
}
|
||||
|
||||
dispatch(inputsActions.setFullScreen({ id: 'global', fullScreen }));
|
||||
|
|
|
@ -79,24 +79,41 @@ export function dataAccessLayerFactory(
|
|||
timeRange: TimeRange;
|
||||
indexPatterns: string[];
|
||||
}): Promise<ResolverPaginatedEvents> {
|
||||
return context.services.http.post('/api/endpoint/resolver/events', {
|
||||
const commonFields = {
|
||||
query: { afterEvent: after, limit: 25 },
|
||||
body: JSON.stringify({
|
||||
body: {
|
||||
timeRange: {
|
||||
from: timeRange.from,
|
||||
to: timeRange.to,
|
||||
},
|
||||
indexPatterns,
|
||||
filter: JSON.stringify({
|
||||
bool: {
|
||||
filter: [
|
||||
{ term: { 'process.entity_id': entityID } },
|
||||
{ term: { 'event.category': category } },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
if (category === 'alerts') {
|
||||
return context.services.http.post('/api/endpoint/resolver/events', {
|
||||
query: commonFields.query,
|
||||
body: JSON.stringify({
|
||||
...commonFields,
|
||||
entityType: 'alerts',
|
||||
eventID: entityID,
|
||||
}),
|
||||
}),
|
||||
});
|
||||
});
|
||||
} else {
|
||||
return context.services.http.post('/api/endpoint/resolver/events', {
|
||||
query: commonFields.query,
|
||||
body: JSON.stringify({
|
||||
...commonFields,
|
||||
filter: JSON.stringify({
|
||||
bool: {
|
||||
filter: [
|
||||
{ term: { 'process.entity_id': entityID } },
|
||||
{ term: { 'event.category': category } },
|
||||
],
|
||||
},
|
||||
}),
|
||||
}),
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -176,22 +193,42 @@ export function dataAccessLayerFactory(
|
|||
filter: [{ term: { 'event.id': eventID } }],
|
||||
},
|
||||
};
|
||||
const response: ResolverPaginatedEvents = await context.services.http.post(
|
||||
'/api/endpoint/resolver/events',
|
||||
{
|
||||
query: { limit: 1 },
|
||||
body: JSON.stringify({
|
||||
indexPatterns,
|
||||
timeRange: {
|
||||
from: timeRange.from,
|
||||
to: timeRange.to,
|
||||
},
|
||||
filter: JSON.stringify(filter),
|
||||
}),
|
||||
}
|
||||
);
|
||||
const [oneEvent] = response.events;
|
||||
return oneEvent ?? null;
|
||||
if (eventCategory.includes('alerts') === false) {
|
||||
const response: ResolverPaginatedEvents = await context.services.http.post(
|
||||
'/api/endpoint/resolver/events',
|
||||
{
|
||||
query: { limit: 1 },
|
||||
body: JSON.stringify({
|
||||
indexPatterns,
|
||||
timeRange: {
|
||||
from: timeRange.from,
|
||||
to: timeRange.to,
|
||||
},
|
||||
filter: JSON.stringify(filter),
|
||||
}),
|
||||
}
|
||||
);
|
||||
const [oneEvent] = response.events;
|
||||
return oneEvent ?? null;
|
||||
} else {
|
||||
const response: ResolverPaginatedEvents = await context.services.http.post(
|
||||
'/api/endpoint/resolver/events',
|
||||
{
|
||||
query: { limit: 1 },
|
||||
body: JSON.stringify({
|
||||
indexPatterns,
|
||||
timeRange: {
|
||||
from: timeRange.from,
|
||||
to: timeRange.to,
|
||||
},
|
||||
entityType: 'alertDetail',
|
||||
eventID,
|
||||
}),
|
||||
}
|
||||
);
|
||||
const [oneEvent] = response.events;
|
||||
return oneEvent ?? null;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
@ -39,6 +39,7 @@ describe(`Resolver: when analyzing a tree with no ancestors and two children and
|
|||
['@timestamp', 'Sep 23, 2020 @ 08:25:32.316'],
|
||||
['process.executable', 'executable'],
|
||||
['process.pid', '0'],
|
||||
['process.entity_id', 'origin'],
|
||||
['user.name', 'user.name'],
|
||||
['user.domain', 'user.domain'],
|
||||
['process.parent.pid', '0'],
|
||||
|
@ -186,6 +187,7 @@ describe(`Resolver: when analyzing a tree with no ancestors and two children and
|
|||
['@timestamp', 'Sep 23, 2020 @ 08:25:32.317'],
|
||||
['process.executable', 'executable'],
|
||||
['process.pid', '1'],
|
||||
['process.entity_id', 'firstChild'],
|
||||
['user.name', 'user.name'],
|
||||
['user.domain', 'user.domain'],
|
||||
['process.parent.pid', '0'],
|
||||
|
|
|
@ -55,6 +55,18 @@ export function DescriptiveName({ event }: { event: SafeResolverEvent }) {
|
|||
);
|
||||
}
|
||||
|
||||
if (event.kibana?.alert?.rule?.name) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.resolver.eventDescription.alertEventNameLabel"
|
||||
defaultMessage="{ ruleName }"
|
||||
values={{
|
||||
ruleName: String(event.kibana?.alert?.rule?.name),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (event.file?.path) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
|
|
|
@ -160,7 +160,7 @@ function EventDetailFields({ event }: { event: SafeResolverEvent }) {
|
|||
}> = [];
|
||||
for (const [key, value] of Object.entries(event)) {
|
||||
// ignore these keys
|
||||
if (key === 'agent' || key === 'ecs' || key === 'process' || key === '@timestamp') {
|
||||
if (key === 'agent' || key === 'ecs' || key === '@timestamp') {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
|
@ -103,6 +103,11 @@ const NodeDetailView = memo(function ({
|
|||
description: eventModel.userName(processEvent),
|
||||
};
|
||||
|
||||
const processEntityId = {
|
||||
title: 'process.entity_id',
|
||||
description: eventModel.entityId(processEvent),
|
||||
};
|
||||
|
||||
const domainEntry = {
|
||||
title: 'user.domain',
|
||||
description: eventModel.userDomain(processEvent),
|
||||
|
@ -132,6 +137,7 @@ const NodeDetailView = memo(function ({
|
|||
createdEntry,
|
||||
pathEntry,
|
||||
pidEntry,
|
||||
processEntityId,
|
||||
userEntry,
|
||||
domainEntry,
|
||||
parentPidEntry,
|
||||
|
|
|
@ -32,6 +32,7 @@ import { DescriptiveName } from './descriptive_name';
|
|||
import { useLinkProps } from '../use_link_props';
|
||||
import { useResolverDispatch } from '../use_resolver_dispatch';
|
||||
import { useFormattedDate } from './use_formatted_date';
|
||||
import { expandDottedObject } from '../../../../common/utils/expand_dotted';
|
||||
|
||||
/**
|
||||
* Render a list of events that are related to `nodeID` and that have a category of `eventType`.
|
||||
|
@ -104,7 +105,7 @@ const NodeEventsListItem = memo(function ({
|
|||
eventCategory: string;
|
||||
}) {
|
||||
const timestamp = eventModel.eventTimestamp(event);
|
||||
const eventID = eventModel.eventID(event);
|
||||
const eventID = eventModel.eventID(expandDottedObject(event));
|
||||
const winlogRecordID = eventModel.winlogRecordID(event);
|
||||
const date =
|
||||
useFormattedDate(timestamp) ||
|
||||
|
|
|
@ -27,14 +27,7 @@ import { timelineDefaults } from '../../store/timeline/defaults';
|
|||
import { isFullScreen } from '../timeline/body/column_headers';
|
||||
import { inputsActions } from '../../../common/store/actions';
|
||||
import { Resolver } from '../../../resolver/view';
|
||||
import {
|
||||
isLoadingSelector,
|
||||
startSelector,
|
||||
endSelector,
|
||||
} from '../../../common/components/super_date_picker/selectors';
|
||||
import { SourcererScopeName } from '../../../common/store/sourcerer/model';
|
||||
import { useSourcererDataView } from '../../../common/containers/sourcerer';
|
||||
import { sourcererSelectors } from '../../../common/store';
|
||||
import { useTimelineDataFilters } from '../../containers/use_timeline_data_filters';
|
||||
|
||||
const SESSION_VIEW_FULL_SCREEN = 'sessionViewFullScreen';
|
||||
|
||||
|
@ -98,32 +91,6 @@ const GraphOverlayComponent: React.FC<GraphOverlayProps> = ({
|
|||
(state) => (getTimeline(state, timelineId) ?? timelineDefaults).sessionViewConfig
|
||||
);
|
||||
|
||||
const getStartSelector = useMemo(() => startSelector(), []);
|
||||
const getEndSelector = useMemo(() => endSelector(), []);
|
||||
const getIsLoadingSelector = useMemo(() => isLoadingSelector(), []);
|
||||
const isActive = useMemo(() => timelineId === TimelineId.active, [timelineId]);
|
||||
const shouldUpdate = useDeepEqualSelector((state) => {
|
||||
if (isActive) {
|
||||
return getIsLoadingSelector(state.inputs.timeline);
|
||||
} else {
|
||||
return getIsLoadingSelector(state.inputs.global);
|
||||
}
|
||||
});
|
||||
const from = useDeepEqualSelector((state) => {
|
||||
if (isActive) {
|
||||
return getStartSelector(state.inputs.timeline);
|
||||
} else {
|
||||
return getStartSelector(state.inputs.global);
|
||||
}
|
||||
});
|
||||
const to = useDeepEqualSelector((state) => {
|
||||
if (isActive) {
|
||||
return getEndSelector(state.inputs.timeline);
|
||||
} else {
|
||||
return getEndSelector(state.inputs.global);
|
||||
}
|
||||
});
|
||||
|
||||
const fullScreen = useMemo(
|
||||
() => isFullScreen({ globalFullScreen, timelineId, timelineFullScreen }),
|
||||
[globalFullScreen, timelineId, timelineFullScreen]
|
||||
|
@ -141,18 +108,7 @@ const GraphOverlayComponent: React.FC<GraphOverlayProps> = ({
|
|||
};
|
||||
}, [dispatch, timelineId]);
|
||||
|
||||
const getDefaultDataViewSelector = useMemo(
|
||||
() => sourcererSelectors.defaultDataViewSelector(),
|
||||
[]
|
||||
);
|
||||
const defaultDataView = useDeepEqualSelector(getDefaultDataViewSelector);
|
||||
|
||||
const { selectedPatterns: timelinePatterns } = useSourcererDataView(SourcererScopeName.timeline);
|
||||
|
||||
const selectedPatterns = useMemo(
|
||||
() => (isInTimeline ? timelinePatterns : defaultDataView.patternList),
|
||||
[defaultDataView.patternList, isInTimeline, timelinePatterns]
|
||||
);
|
||||
const { from, to, shouldUpdate, selectedPatterns } = useTimelineDataFilters(timelineId);
|
||||
|
||||
const sessionContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
|
|
|
@ -66,7 +66,6 @@ export const useTimelineEventsDetails = ({
|
|||
const [ecsData, setEcsData] = useState<EventsArgs['ecs']>(null);
|
||||
|
||||
const [rawEventData, setRawEventData] = useState<object | undefined>(undefined);
|
||||
|
||||
const timelineDetailsSearch = useCallback(
|
||||
(request: TimelineEventsDetailsRequestOptions | null) => {
|
||||
if (request == null || skip || isEmpty(request.eventId)) {
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { useDeepEqualSelector } from '../../common/hooks/use_selector';
|
||||
import { TimelineId } from '../../../common/types/timeline';
|
||||
import {
|
||||
isLoadingSelector,
|
||||
startSelector,
|
||||
endSelector,
|
||||
} from '../../common/components/super_date_picker/selectors';
|
||||
import { SourcererScopeName } from '../../common/store/sourcerer/model';
|
||||
import { useSourcererDataView } from '../../common/containers/sourcerer';
|
||||
import { sourcererSelectors } from '../../common/store';
|
||||
|
||||
export function useTimelineDataFilters(timelineId: string | undefined) {
|
||||
const getStartSelector = useMemo(() => startSelector(), []);
|
||||
const getEndSelector = useMemo(() => endSelector(), []);
|
||||
const getIsLoadingSelector = useMemo(() => isLoadingSelector(), []);
|
||||
const isActive = useMemo(() => timelineId === TimelineId.active, [timelineId]);
|
||||
const isInTimeline = timelineId === TimelineId.active;
|
||||
|
||||
const shouldUpdate = useDeepEqualSelector((state) => {
|
||||
if (isActive) {
|
||||
return getIsLoadingSelector(state.inputs.timeline);
|
||||
} else {
|
||||
return getIsLoadingSelector(state.inputs.global);
|
||||
}
|
||||
});
|
||||
const from = useDeepEqualSelector((state) => {
|
||||
if (isActive) {
|
||||
return getStartSelector(state.inputs.timeline);
|
||||
} else {
|
||||
return getStartSelector(state.inputs.global);
|
||||
}
|
||||
});
|
||||
const to = useDeepEqualSelector((state) => {
|
||||
if (isActive) {
|
||||
return getEndSelector(state.inputs.timeline);
|
||||
} else {
|
||||
return getEndSelector(state.inputs.global);
|
||||
}
|
||||
});
|
||||
const getDefaultDataViewSelector = useMemo(
|
||||
() => sourcererSelectors.defaultDataViewSelector(),
|
||||
[]
|
||||
);
|
||||
const defaultDataView = useDeepEqualSelector(getDefaultDataViewSelector);
|
||||
|
||||
const { selectedPatterns: timelinePatterns } = useSourcererDataView(SourcererScopeName.timeline);
|
||||
|
||||
const selectedPatterns = useMemo(
|
||||
() => (isInTimeline ? timelinePatterns : defaultDataView.patternList),
|
||||
[defaultDataView.patternList, isInTimeline, timelinePatterns]
|
||||
);
|
||||
|
||||
return {
|
||||
selectedPatterns,
|
||||
from,
|
||||
to,
|
||||
shouldUpdate,
|
||||
};
|
||||
}
|
|
@ -4,8 +4,9 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { IRouter } from '@kbn/core/server';
|
||||
import type { StartServicesAccessor } from '@kbn/core/server';
|
||||
import type { SecuritySolutionPluginRouter } from '../../types';
|
||||
import type { StartPlugins } from '../../plugin';
|
||||
import {
|
||||
validateEvents,
|
||||
validateEntities,
|
||||
|
@ -16,14 +17,18 @@ import { handleTree } from './resolver/tree/handler';
|
|||
import { handleEntities } from './resolver/entity/handler';
|
||||
import { handleEvents } from './resolver/events';
|
||||
|
||||
export function registerResolverRoutes(router: IRouter) {
|
||||
export const registerResolverRoutes = async (
|
||||
router: SecuritySolutionPluginRouter,
|
||||
startServices: StartServicesAccessor<StartPlugins>
|
||||
) => {
|
||||
const [, { ruleRegistry }] = await startServices();
|
||||
router.post(
|
||||
{
|
||||
path: '/api/endpoint/resolver/tree',
|
||||
validate: validateTree,
|
||||
options: { authRequired: true },
|
||||
},
|
||||
handleTree()
|
||||
handleTree(ruleRegistry)
|
||||
);
|
||||
|
||||
router.post(
|
||||
|
@ -32,7 +37,7 @@ export function registerResolverRoutes(router: IRouter) {
|
|||
validate: validateEvents,
|
||||
options: { authRequired: true },
|
||||
},
|
||||
handleEvents()
|
||||
handleEvents(ruleRegistry)
|
||||
);
|
||||
|
||||
/**
|
||||
|
@ -46,4 +51,4 @@ export function registerResolverRoutes(router: IRouter) {
|
|||
},
|
||||
handleEntities()
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import type { TypeOf } from '@kbn/config-schema';
|
||||
import type { RequestHandler } from '@kbn/core/server';
|
||||
import type { RuleRegistryPluginStartContract } from '@kbn/rule-registry-plugin/server';
|
||||
import type { ResolverPaginatedEvents, SafeResolverEvent } from '../../../../common/endpoint/types';
|
||||
import type { validateEvents } from '../../../../common/endpoint/schema/resolver';
|
||||
import { EventsQuery } from './queries/events';
|
||||
|
@ -29,7 +30,9 @@ function createEvents(
|
|||
* This function handles the `/events` api and returns an array of events and a cursor if more events exist than were
|
||||
* requested.
|
||||
*/
|
||||
export function handleEvents(): RequestHandler<
|
||||
export function handleEvents(
|
||||
ruleRegistry: RuleRegistryPluginStartContract
|
||||
): RequestHandler<
|
||||
unknown,
|
||||
TypeOf<typeof validateEvents.query>,
|
||||
TypeOf<typeof validateEvents.body>
|
||||
|
@ -39,13 +42,15 @@ export function handleEvents(): RequestHandler<
|
|||
query: { limit, afterEvent },
|
||||
body,
|
||||
} = req;
|
||||
const client = (await context.core).elasticsearch.client;
|
||||
const query = new EventsQuery({
|
||||
const eventsClient = (await context.core).elasticsearch.client;
|
||||
const alertsClient = await ruleRegistry.getRacClientWithRequest(req);
|
||||
|
||||
const eventsQuery = new EventsQuery({
|
||||
pagination: PaginationBuilder.createBuilder(limit, afterEvent),
|
||||
indexPatterns: body.indexPatterns,
|
||||
timeRange: body.timeRange,
|
||||
});
|
||||
const results = await query.search(client, body.filter);
|
||||
const results = await eventsQuery.search(eventsClient, body, alertsClient);
|
||||
return res.ok({
|
||||
body: createEvents(results, PaginationBuilder.buildCursorRequestLimit(limit, results)),
|
||||
});
|
||||
|
|
|
@ -6,7 +6,8 @@
|
|||
*/
|
||||
|
||||
import type { IScopedClusterClient } from '@kbn/core/server';
|
||||
import type { JsonObject } from '@kbn/utility-types';
|
||||
import type { AlertsClient } from '@kbn/rule-registry-plugin/server';
|
||||
import type { JsonObject, JsonValue } from '@kbn/utility-types';
|
||||
import { parseFilterQuery } from '../../../../utils/serialized_query';
|
||||
import type { SafeResolverEvent } from '../../../../../common/endpoint/types';
|
||||
import type { PaginationBuilder } from '../utils/pagination';
|
||||
|
@ -62,6 +63,58 @@ export class EventsQuery {
|
|||
};
|
||||
}
|
||||
|
||||
private alertDetailQuery(id?: JsonValue): { query: object; index: string } {
|
||||
return {
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
term: { 'event.id': id },
|
||||
},
|
||||
{
|
||||
range: {
|
||||
'@timestamp': {
|
||||
gte: this.timeRange.from,
|
||||
lte: this.timeRange.to,
|
||||
format: 'strict_date_optional_time',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
index:
|
||||
typeof this.indexPatterns === 'string' ? this.indexPatterns : this.indexPatterns.join(','),
|
||||
...this.pagination.buildQueryFields('event.id', 'desc'),
|
||||
};
|
||||
}
|
||||
|
||||
private alertsForProcessQuery(id?: JsonValue): { query: object; index: string } {
|
||||
return {
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
term: { 'process.entity_id': id },
|
||||
},
|
||||
{
|
||||
range: {
|
||||
'@timestamp': {
|
||||
gte: this.timeRange.from,
|
||||
lte: this.timeRange.to,
|
||||
format: 'strict_date_optional_time',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
index:
|
||||
typeof this.indexPatterns === 'string' ? this.indexPatterns : this.indexPatterns.join(','),
|
||||
...this.pagination.buildQueryFields('event.id', 'desc'),
|
||||
};
|
||||
}
|
||||
|
||||
private buildSearch(filters: JsonObject[]) {
|
||||
return {
|
||||
body: this.query(filters),
|
||||
|
@ -78,20 +131,34 @@ export class EventsQuery {
|
|||
}
|
||||
|
||||
/**
|
||||
* Searches ES for the specified events and format the response.
|
||||
* Will search ES using a filter for normal events associated with a process, or an entity type and event id for alert events.
|
||||
*
|
||||
* @param client a client for searching ES
|
||||
* @param filter an optional string representation of a raw Elasticsearch clause for filtering the results
|
||||
*/
|
||||
async search(
|
||||
client: IScopedClusterClient,
|
||||
filter: string | undefined
|
||||
body: { filter?: string; eventID?: string; entityType?: string },
|
||||
alertsClient: AlertsClient
|
||||
): Promise<SafeResolverEvent[]> {
|
||||
const parsedFilters = EventsQuery.buildFilters(filter);
|
||||
const response = await client.asCurrentUser.search<SafeResolverEvent>(
|
||||
this.buildSearch(parsedFilters)
|
||||
);
|
||||
// @ts-expect-error @elastic/elasticsearch _source is optional
|
||||
return response.hits.hits.map((hit) => hit._source);
|
||||
if (body.filter) {
|
||||
const parsedFilters = EventsQuery.buildFilters(body.filter);
|
||||
const response = await client.asCurrentUser.search<SafeResolverEvent>(
|
||||
this.buildSearch(parsedFilters)
|
||||
);
|
||||
// @ts-expect-error @elastic/elasticsearch _source is optional
|
||||
return response.hits.hits.map((hit) => hit._source);
|
||||
} else {
|
||||
const { eventID, entityType } = body;
|
||||
if (entityType === 'alertDetail') {
|
||||
const response = await alertsClient.find(this.alertDetailQuery(eventID));
|
||||
// @ts-expect-error @elastic/elasticsearch _source is optional
|
||||
return response.hits.hits.map((hit) => hit._source);
|
||||
} else {
|
||||
const response = await alertsClient.find(this.alertsForProcessQuery(eventID));
|
||||
// @ts-expect-error @elastic/elasticsearch _source is optional
|
||||
return response.hits.hits.map((hit) => hit._source);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,13 +7,17 @@
|
|||
|
||||
import type { RequestHandler } from '@kbn/core/server';
|
||||
import type { TypeOf } from '@kbn/config-schema';
|
||||
import type { RuleRegistryPluginStartContract } from '@kbn/rule-registry-plugin/server';
|
||||
import type { validateTree } from '../../../../../common/endpoint/schema/resolver';
|
||||
import { Fetcher } from './utils/fetch';
|
||||
|
||||
export function handleTree(): RequestHandler<unknown, unknown, TypeOf<typeof validateTree.body>> {
|
||||
export function handleTree(
|
||||
ruleRegistry: RuleRegistryPluginStartContract
|
||||
): RequestHandler<unknown, unknown, TypeOf<typeof validateTree.body>> {
|
||||
return async (context, req, res) => {
|
||||
const client = (await context.core).elasticsearch.client;
|
||||
const fetcher = new Fetcher(client);
|
||||
const alertsClient = await ruleRegistry.getRacClientWithRequest(req);
|
||||
const fetcher = new Fetcher(client, alertsClient);
|
||||
const body = await fetcher.tree(req.body);
|
||||
return res.ok({
|
||||
body,
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
*/
|
||||
|
||||
import type { IScopedClusterClient } from '@kbn/core/server';
|
||||
import type { AlertsClient } from '@kbn/rule-registry-plugin/server';
|
||||
import { ALERT_RULE_UUID } from '@kbn/rule-data-utils';
|
||||
import type { JsonObject } from '@kbn/utility-types';
|
||||
import type { EventStats, ResolverSchema } from '../../../../../../common/endpoint/types';
|
||||
import type { NodeID, TimeRange } from '../utils';
|
||||
|
@ -94,6 +96,40 @@ export class StatsQuery {
|
|||
};
|
||||
}
|
||||
|
||||
private alertStatsQuery(
|
||||
nodes: NodeID[],
|
||||
index: string,
|
||||
includeHits: boolean
|
||||
): { size: number; query: object; index: string; aggs: object; fields?: string[] } {
|
||||
return {
|
||||
size: includeHits ? 5000 : 0,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
range: {
|
||||
'@timestamp': {
|
||||
gte: this.timeRange.from,
|
||||
lte: this.timeRange.to,
|
||||
format: 'strict_date_optional_time',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
terms: { [this.schema.id]: nodes },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
index,
|
||||
aggs: {
|
||||
ids: {
|
||||
terms: { field: this.schema.id },
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private static getEventStats(catAgg: CategoriesAgg): EventStats {
|
||||
const total = catAgg.doc_count;
|
||||
if (!catAgg.categories?.buckets) {
|
||||
|
@ -121,26 +157,99 @@ export class StatsQuery {
|
|||
* @param client used to make requests to Elasticsearch
|
||||
* @param nodes an array of unique IDs representing nodes in a resolver graph
|
||||
*/
|
||||
async search(client: IScopedClusterClient, nodes: NodeID[]): Promise<Record<string, EventStats>> {
|
||||
async search(
|
||||
client: IScopedClusterClient,
|
||||
nodes: NodeID[],
|
||||
alertsClient: AlertsClient | undefined,
|
||||
includeHits: boolean
|
||||
): Promise<{ eventStats?: Record<string, EventStats>; alertIds?: string[] }> {
|
||||
if (nodes.length <= 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const esClient = this.isInternalRequest ? client.asInternalUser : client.asCurrentUser;
|
||||
const alertIndex =
|
||||
typeof this.indexPatterns === 'string' ? this.indexPatterns : this.indexPatterns.join(',');
|
||||
|
||||
// leaving unknown here because we don't actually need the hits part of the body
|
||||
const body = await esClient.search({
|
||||
body: this.query(nodes),
|
||||
index: this.indexPatterns,
|
||||
});
|
||||
|
||||
// @ts-expect-error declare aggegations type explicitly
|
||||
return body.aggregations?.ids?.buckets.reduce(
|
||||
(cummulative: Record<string, number>, bucket: CategoriesAgg) => ({
|
||||
...cummulative,
|
||||
[bucket.key]: StatsQuery.getEventStats(bucket),
|
||||
const [body, alertsBody] = await Promise.all([
|
||||
await esClient.search({
|
||||
body: this.query(nodes),
|
||||
index: this.indexPatterns,
|
||||
}),
|
||||
alertsClient
|
||||
? await alertsClient.find(this.alertStatsQuery(nodes, alertIndex, includeHits))
|
||||
: { hits: { hits: [] } },
|
||||
]);
|
||||
// @ts-expect-error declare aggegations type explicitly
|
||||
const eventAggs: CategoriesAgg[] = body.aggregations?.ids?.buckets ?? [];
|
||||
// @ts-expect-error declare aggegations type explicitly
|
||||
const alertAggs: AggBucket[] = alertsBody.aggregations?.ids?.buckets ?? [];
|
||||
const eventsWithAggs = new Set([
|
||||
...eventAggs.map((agg) => agg.key),
|
||||
...alertAggs.map((agg) => agg.key),
|
||||
]);
|
||||
const alertsAggsMap = new Map(alertAggs.map(({ key, doc_count: docCount }) => [key, docCount]));
|
||||
const eventAggsMap = new Map<string, EventStats>(
|
||||
eventAggs.map(({ key, doc_count: docCount, categories }): [string, EventStats] => [
|
||||
key,
|
||||
{
|
||||
...StatsQuery.getEventStats({ key, doc_count: docCount, categories }),
|
||||
},
|
||||
])
|
||||
);
|
||||
const alertIdsRaw: Array<string | undefined> = alertsBody.hits.hits.map((hit) => {
|
||||
return hit._source && hit._source[ALERT_RULE_UUID];
|
||||
});
|
||||
const alertIds = alertIdsRaw.flatMap((id) => (!id ? [] : [id]));
|
||||
|
||||
const eventAggStats = [...eventsWithAggs.values()];
|
||||
const eventStats = eventAggStats.reduce(
|
||||
(cummulative: Record<string, EventStats>, id: string) => {
|
||||
const alertCount = alertsAggsMap.get(id);
|
||||
const otherEvents = eventAggsMap.get(id);
|
||||
if (alertCount !== undefined) {
|
||||
if (otherEvents !== undefined) {
|
||||
return {
|
||||
...cummulative,
|
||||
[id]: {
|
||||
total: alertCount + otherEvents.total,
|
||||
byCategory: {
|
||||
alerts: alertCount,
|
||||
...otherEvents.byCategory,
|
||||
},
|
||||
},
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
...cummulative,
|
||||
[id]: {
|
||||
total: alertCount,
|
||||
byCategory: {
|
||||
alerts: alertCount,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
} else {
|
||||
if (otherEvents !== undefined) {
|
||||
return {
|
||||
...cummulative,
|
||||
[id]: {
|
||||
total: otherEvents.total,
|
||||
byCategory: otherEvents.byCategory,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
},
|
||||
{}
|
||||
);
|
||||
if (includeHits) {
|
||||
return { alertIds, eventStats };
|
||||
} else {
|
||||
return { eventStats };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import type { IScopedClusterClient } from '@kbn/core/server';
|
||||
import type { AlertsClient } from '@kbn/rule-registry-plugin/server';
|
||||
import {
|
||||
firstNonNullValue,
|
||||
values,
|
||||
|
@ -36,23 +37,32 @@ export interface TreeOptions {
|
|||
schema: ResolverSchema;
|
||||
nodes: NodeID[];
|
||||
indexPatterns: string[];
|
||||
includeHits?: boolean;
|
||||
}
|
||||
|
||||
export type TreeResponse = Promise<
|
||||
| ResolverNode[]
|
||||
| {
|
||||
alertIds: string[] | undefined;
|
||||
statsNodes: ResolverNode[];
|
||||
}
|
||||
>;
|
||||
|
||||
/**
|
||||
* Handles retrieving nodes of a resolver tree.
|
||||
*/
|
||||
export class Fetcher {
|
||||
constructor(private readonly client: IScopedClusterClient) {}
|
||||
private alertsClient?: AlertsClient;
|
||||
constructor(private readonly client: IScopedClusterClient, alertsClient?: AlertsClient) {
|
||||
this.alertsClient = alertsClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method retrieves the ancestors and descendants of a resolver tree.
|
||||
*
|
||||
* @param options the options for retrieving the structure of the tree.
|
||||
*/
|
||||
public async tree(
|
||||
options: TreeOptions,
|
||||
isInternalRequest: boolean = false
|
||||
): Promise<ResolverNode[]> {
|
||||
public async tree(options: TreeOptions, isInternalRequest: boolean = false): TreeResponse {
|
||||
const treeParts = await Promise.all([
|
||||
this.retrieveAncestors(options, isInternalRequest),
|
||||
this.retrieveDescendants(options, isInternalRequest),
|
||||
|
@ -70,7 +80,7 @@ export class Fetcher {
|
|||
treeNodes: FieldsObject[],
|
||||
options: TreeOptions,
|
||||
isInternalRequest: boolean
|
||||
): Promise<ResolverNode[]> {
|
||||
): TreeResponse {
|
||||
const statsIDs: NodeID[] = [];
|
||||
for (const node of treeNodes) {
|
||||
const id = getIDField(node, options.schema);
|
||||
|
@ -86,7 +96,12 @@ export class Fetcher {
|
|||
isInternalRequest,
|
||||
});
|
||||
|
||||
const eventStats = await query.search(this.client, statsIDs);
|
||||
const { eventStats, alertIds } = await query.search(
|
||||
this.client,
|
||||
statsIDs,
|
||||
this.alertsClient,
|
||||
options.includeHits ?? false
|
||||
);
|
||||
const statsNodes: ResolverNode[] = [];
|
||||
for (const node of treeNodes) {
|
||||
const id = getIDField(node, options.schema);
|
||||
|
@ -96,16 +111,21 @@ export class Fetcher {
|
|||
// at this point id should never be undefined, it should be enforced by the Elasticsearch query
|
||||
// but let's check anyway
|
||||
if (id !== undefined) {
|
||||
const stats = (eventStats && eventStats[id]) ?? { total: 0, byCategory: {} };
|
||||
statsNodes.push({
|
||||
id,
|
||||
parent,
|
||||
name,
|
||||
data: node,
|
||||
stats: eventStats[id] ?? { total: 0, byCategory: {} },
|
||||
stats,
|
||||
});
|
||||
}
|
||||
}
|
||||
return statsNodes;
|
||||
if (options.includeHits) {
|
||||
return { alertIds, statsNodes };
|
||||
} else {
|
||||
return statsNodes;
|
||||
}
|
||||
}
|
||||
|
||||
private static getNextAncestorsToFind(
|
||||
|
|
|
@ -123,6 +123,9 @@ const createSecuritySolutionRequestContextMock = (
|
|||
};
|
||||
}),
|
||||
getAppClient: jest.fn(() => clients.appClient),
|
||||
getRacClient: jest.fn((req: KibanaRequest) => {
|
||||
throw new Error('Not implemented');
|
||||
}),
|
||||
getSpaceId: jest.fn(() => 'default'),
|
||||
getRuleDataService: jest.fn(() => clients.ruleDataService),
|
||||
getRuleExecutionLog: jest.fn(() => clients.ruleExecutionLog),
|
||||
|
|
|
@ -39,12 +39,8 @@ import {
|
|||
ruleExceptionListItemToTelemetryEvent,
|
||||
} from './helpers';
|
||||
import { Fetcher } from '../../endpoint/routes/resolver/tree/utils/fetch';
|
||||
import type { TreeOptions } from '../../endpoint/routes/resolver/tree/utils/fetch';
|
||||
import type {
|
||||
ResolverNode,
|
||||
SafeEndpointEvent,
|
||||
ResolverSchema,
|
||||
} from '../../../common/endpoint/types';
|
||||
import type { TreeOptions, TreeResponse } from '../../endpoint/routes/resolver/tree/utils/fetch';
|
||||
import type { SafeEndpointEvent, ResolverSchema } from '../../../common/endpoint/types';
|
||||
import type {
|
||||
TelemetryEvent,
|
||||
EnhancedAlertEvent,
|
||||
|
@ -153,7 +149,7 @@ export interface ITelemetryReceiver {
|
|||
resolverSchema: ResolverSchema,
|
||||
startOfDay: string,
|
||||
endOfDay: string
|
||||
): Promise<ResolverNode[]>;
|
||||
): TreeResponse;
|
||||
|
||||
fetchTimelineEvents(
|
||||
nodeIds: string[]
|
||||
|
@ -739,7 +735,7 @@ export class TelemetryReceiver implements ITelemetryReceiver {
|
|||
resolverSchema: ResolverSchema,
|
||||
startOfDay: string,
|
||||
endOfDay: string
|
||||
): Promise<ResolverNode[]> {
|
||||
): TreeResponse {
|
||||
if (this.processTreeFetcher === undefined || this.processTreeFetcher === null) {
|
||||
throw Error(
|
||||
'resolver tree builder is unavailable: cannot build encoded endpoint event graph'
|
||||
|
|
|
@ -104,9 +104,11 @@ export function createTelemetryTimelineTaskConfig() {
|
|||
);
|
||||
|
||||
const nodeIds = [] as string[];
|
||||
for (const node of tree) {
|
||||
const nodeId = node?.id.toString();
|
||||
nodeIds.push(nodeId);
|
||||
if (Array.isArray(tree)) {
|
||||
for (const node of tree) {
|
||||
const nodeId = node?.id.toString();
|
||||
nodeIds.push(nodeId);
|
||||
}
|
||||
}
|
||||
|
||||
sender.getTelemetryUsageCluster()?.incrementCounter({
|
||||
|
@ -138,16 +140,18 @@ export function createTelemetryTimelineTaskConfig() {
|
|||
// Create telemetry record
|
||||
|
||||
const telemetryTimeline: TimelineTelemetryEvent[] = [];
|
||||
for (const node of tree) {
|
||||
const id = node.id.toString();
|
||||
const event = eventsStore.get(id);
|
||||
if (Array.isArray(tree)) {
|
||||
for (const node of tree) {
|
||||
const id = node.id.toString();
|
||||
const event = eventsStore.get(id);
|
||||
|
||||
const timelineTelemetryEvent: TimelineTelemetryEvent = {
|
||||
...node,
|
||||
event,
|
||||
};
|
||||
const timelineTelemetryEvent: TimelineTelemetryEvent = {
|
||||
...node,
|
||||
event,
|
||||
};
|
||||
|
||||
telemetryTimeline.push(timelineTelemetryEvent);
|
||||
telemetryTimeline.push(timelineTelemetryEvent);
|
||||
}
|
||||
}
|
||||
|
||||
if (telemetryTimeline.length >= 1) {
|
||||
|
|
|
@ -54,7 +54,6 @@ import {
|
|||
DEFAULT_ALERTS_INDEX,
|
||||
} from '../common/constants';
|
||||
import { registerEndpointRoutes } from './endpoint/routes/metadata';
|
||||
import { registerResolverRoutes } from './endpoint/routes/resolver';
|
||||
import { registerPolicyRoutes } from './endpoint/routes/policy';
|
||||
import { registerActionRoutes } from './endpoint/routes/actions';
|
||||
import { EndpointArtifactClient, ManifestManager } from './endpoint/services';
|
||||
|
@ -275,7 +274,6 @@ export class Plugin implements ISecuritySolutionPlugin {
|
|||
);
|
||||
registerEndpointRoutes(router, endpointContext);
|
||||
registerLimitedConcurrencyRoutes(core);
|
||||
registerResolverRoutes(router);
|
||||
registerPolicyRoutes(router, endpointContext);
|
||||
registerActionRoutes(router, endpointContext);
|
||||
|
||||
|
|
|
@ -109,6 +109,8 @@ export class RequestContextFactory implements IRequestContextFactory {
|
|||
|
||||
getRuleDataService: () => ruleRegistry.ruleDataService,
|
||||
|
||||
getRacClient: startPlugins.ruleRegistry.getRacClientWithRequest,
|
||||
|
||||
getRuleExecutionLog: memoize(() =>
|
||||
ruleExecutionLogService.createClientForRoutes({
|
||||
savedObjectsClient: coreContext.savedObjects.client,
|
||||
|
|
|
@ -73,6 +73,7 @@ import { readPrebuiltDevToolContentRoute } from '../lib/prebuilt_dev_tool_conten
|
|||
import { createPrebuiltSavedObjectsRoute } from '../lib/prebuilt_saved_objects/routes/create_prebuilt_saved_objects';
|
||||
import { readAlertsIndexExistsRoute } from '../lib/detection_engine/routes/index/read_alerts_index_exists_route';
|
||||
import { getInstalledIntegrationsRoute } from '../lib/detection_engine/routes/fleet/get_installed_integrations/get_installed_integrations_route';
|
||||
import { registerResolverRoutes } from '../endpoint/routes/resolver';
|
||||
|
||||
export const initRoutes = (
|
||||
router: SecuritySolutionPluginRouter,
|
||||
|
@ -119,6 +120,7 @@ export const initRoutes = (
|
|||
patchRulesBulkRoute(router, ml, logger);
|
||||
deleteRulesBulkRoute(router, logger);
|
||||
performBulkActionRoute(router, ml, logger);
|
||||
registerResolverRoutes(router, getStartServices);
|
||||
|
||||
registerRuleMonitoringRoutes(router);
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ import type { AlertingApiRequestHandlerContext } from '@kbn/alerting-plugin/serv
|
|||
import type { FleetRequestHandlerContext } from '@kbn/fleet-plugin/server';
|
||||
import type { LicensingApiRequestHandlerContext } from '@kbn/licensing-plugin/server';
|
||||
import type { ListsApiRequestHandlerContext, ExceptionListClient } from '@kbn/lists-plugin/server';
|
||||
import type { IRuleDataService } from '@kbn/rule-registry-plugin/server';
|
||||
import type { IRuleDataService, AlertsClient } from '@kbn/rule-registry-plugin/server';
|
||||
|
||||
import { AppClient } from './client';
|
||||
import type { ConfigType } from './config';
|
||||
|
@ -39,6 +39,7 @@ export interface SecuritySolutionApiRequestHandlerContext {
|
|||
getSpaceId: () => string;
|
||||
getRuleDataService: () => IRuleDataService;
|
||||
getRuleExecutionLog: () => IRuleExecutionLogForRoutes;
|
||||
getRacClient: (req: KibanaRequest) => Promise<AlertsClient>;
|
||||
getExceptionListClient: () => ExceptionListClient | null;
|
||||
getInternalFleetServices: () => EndpointInternalFleetServicesInterface;
|
||||
getScopedFleetServices: (req: KibanaRequest) => EndpointScopedFleetServicesInterface;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue