+ simulated tooltip content at zoom: 0
+
+
+ mockFootnoteIcon
+
+
+ simulated footnote at isUsingSearch: true
+
+
+ }
+ delay="regular"
+ position="top"
+ title="layer 1"
+ >
+
+
+
+ mockIcon
+
+
+ layer 1
+
+
+
+
+ mockFootnoteIcon
+
+
+
+
+ }
+ className="mapLayTocActions"
+ closePopover={[Function]}
+ display="inlineBlock"
+ hasArrow={true}
+ id="contextMenu"
+ isOpen={false}
+ ownFocus={false}
+ panelPaddingSize="none"
+ withTitle={true}
+>
+ ,
+ "name": "Fit to data",
+ "onClick": [Function],
+ "toolTipContent": null,
+ },
+ Object {
+ "data-test-subj": "layerVisibilityToggleButton",
+ "icon": ,
"name": "Hide layer",
"onClick": [Function],
"toolTipContent": null,
diff --git a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.test.tsx b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.test.tsx
index c7ed5ec74ac7a..95f13574105b7 100644
--- a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.test.tsx
+++ b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.test.tsx
@@ -6,7 +6,7 @@
/* eslint-disable max-classes-per-file */
import React from 'react';
-import { shallowWithIntl } from 'test_utils/enzyme_helpers';
+import { shallow } from 'enzyme';
import { AbstractLayer, ILayer } from '../../../../../../classes/layers/layer';
import { AbstractSource, ISource } from '../../../../../../classes/sources/source';
import { AbstractStyle, IStyle } from '../../../../../../classes/styles/style';
@@ -76,7 +76,7 @@ describe('TOCEntryActionsPopover', () => {
});
test('is rendered', async () => {
- const component = shallowWithIntl();
+ const component = shallow();
// Ensure all promises resolve
await new Promise((resolve) => process.nextTick(resolve));
@@ -87,9 +87,7 @@ describe('TOCEntryActionsPopover', () => {
});
test('should not show edit actions in read only mode', async () => {
- const component = shallowWithIntl(
-
- );
+ const component = shallow();
// Ensure all promises resolve
await new Promise((resolve) => process.nextTick(resolve));
@@ -101,7 +99,22 @@ describe('TOCEntryActionsPopover', () => {
test('should disable fit to data when supportsFitToBounds is false', async () => {
supportsFitToBounds = false;
- const component = shallowWithIntl();
+ const component = shallow();
+
+ // Ensure all promises resolve
+ await new Promise((resolve) => process.nextTick(resolve));
+ // Ensure the state changes are reflected
+ component.update();
+
+ expect(component).toMatchSnapshot();
+ });
+
+ test('should have "show layer" action when layer is not visible', async () => {
+ const layer = new LayerMock();
+ layer.isVisible = () => {
+ return false;
+ };
+ const component = shallow();
// Ensure all promises resolve
await new Promise((resolve) => process.nextTick(resolve));
diff --git a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.tsx b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.tsx
index 5baac0a474ffa..a1b9026fc57da 100644
--- a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.tsx
+++ b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.tsx
@@ -158,7 +158,7 @@ export class TOCEntryActionsPopover extends Component {
: i18n.translate('xpack.maps.layerTocActions.showLayerTitle', {
defaultMessage: 'Show layer',
}),
- icon: ,
+ icon: ,
'data-test-subj': 'layerVisibilityToggleButton',
toolTipContent: null,
onClick: () => {
diff --git a/x-pack/plugins/maps/public/routing/maps_router.js b/x-pack/plugins/maps/public/routing/maps_router.js
index 30b2137399c1e..9992bd7a92ab1 100644
--- a/x-pack/plugins/maps/public/routing/maps_router.js
+++ b/x-pack/plugins/maps/public/routing/maps_router.js
@@ -7,8 +7,11 @@
import React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import { Router, Switch, Route, Redirect } from 'react-router-dom';
-import { getCoreI18n } from '../kibana_services';
-import { createKbnUrlStateStorage } from '../../../../../src/plugins/kibana_utils/public';
+import { getCoreI18n, getToasts } from '../kibana_services';
+import {
+ createKbnUrlStateStorage,
+ withNotifyOnErrors,
+} from '../../../../../src/plugins/kibana_utils/public';
import { getStore } from './store_operations';
import { Provider } from 'react-redux';
import { LoadListAndRender } from './routes/list/load_list_and_render';
@@ -19,7 +22,11 @@ export let kbnUrlStateStorage;
export async function renderApp(context, { appBasePath, element, history, onAppLeave }) {
goToSpecifiedPath = (path) => history.push(path);
- kbnUrlStateStorage = createKbnUrlStateStorage({ useHash: false, history });
+ kbnUrlStateStorage = createKbnUrlStateStorage({
+ useHash: false,
+ history,
+ ...withNotifyOnErrors(getToasts()),
+ });
render(, element);
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_edit/edit_button_flyout.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_edit/edit_button_flyout.tsx
index 86b1c879417bb..14b743997f30a 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_edit/edit_button_flyout.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_edit/edit_button_flyout.tsx
@@ -133,7 +133,7 @@ export const EditButtonFlyout: FC> = ({ closeFlyout, item }
onClose={closeFlyout}
hideCloseButton
aria-labelledby="analyticsEditFlyoutTitle"
- data-test-subj="analyticsEditFlyout"
+ data-test-subj="mlAnalyticsEditFlyout"
>
@@ -297,7 +297,7 @@ export const EditButtonFlyout: FC> = ({ closeFlyout, item }
= ({ coreStart }) => {
+export const JobsListPage: FC<{
+ coreStart: CoreStart;
+ history: ManagementAppMountParams['history'];
+}> = ({ coreStart, history }) => {
const [initialized, setInitialized] = useState(false);
const [accessDenied, setAccessDenied] = useState(false);
const [isMlEnabledInSpace, setIsMlEnabledInSpace] = useState(false);
@@ -128,46 +137,51 @@ export const JobsListPage: FC<{ coreStart: CoreStart }> = ({ coreStart }) => {
return (
-
-
-
-
-
- {i18n.translate('xpack.ml.management.jobsList.jobsListTitle', {
- defaultMessage: 'Machine Learning Jobs',
- })}
-
-
-
-
- {currentTabId === 'anomaly_detection_jobs'
- ? anomalyDetectionDocsLabel
- : analyticsDocsLabel}
-
-
-
-
-
-
-
- {i18n.translate('xpack.ml.management.jobsList.jobsListTagline', {
- defaultMessage: 'View machine learning analytics and anomaly detection jobs.',
- })}
-
-
-
- {renderTabs()}
-
+
+
+
+
+
+
+ {i18n.translate('xpack.ml.management.jobsList.jobsListTitle', {
+ defaultMessage: 'Machine Learning Jobs',
+ })}
+
+
+
+
+ {currentTabId === 'anomaly_detection_jobs'
+ ? anomalyDetectionDocsLabel
+ : analyticsDocsLabel}
+
+
+
+
+
+
+
+ {i18n.translate('xpack.ml.management.jobsList.jobsListTagline', {
+ defaultMessage: 'View machine learning analytics and anomaly detection jobs.',
+ })}
+
+
+
+ {renderTabs()}
+
+
);
diff --git a/x-pack/plugins/ml/public/application/management/jobs_list/index.ts b/x-pack/plugins/ml/public/application/management/jobs_list/index.ts
index 81190a412abc0..afea5a573b8b5 100644
--- a/x-pack/plugins/ml/public/application/management/jobs_list/index.ts
+++ b/x-pack/plugins/ml/public/application/management/jobs_list/index.ts
@@ -14,8 +14,12 @@ import { getJobsListBreadcrumbs } from '../breadcrumbs';
import { setDependencyCache, clearCache } from '../../util/dependency_cache';
import './_index.scss';
-const renderApp = (element: HTMLElement, coreStart: CoreStart) => {
- ReactDOM.render(React.createElement(JobsListPage, { coreStart }), element);
+const renderApp = (
+ element: HTMLElement,
+ history: ManagementAppMountParams['history'],
+ coreStart: CoreStart
+) => {
+ ReactDOM.render(React.createElement(JobsListPage, { coreStart, history }), element);
return () => {
unmountComponentAtNode(element);
clearCache();
@@ -37,5 +41,5 @@ export async function mountApp(
params.setBreadcrumbs(getJobsListBreadcrumbs());
- return renderApp(params.element, coreStart);
+ return renderApp(params.element, params.history, coreStart);
}
diff --git a/x-pack/plugins/monitoring/public/angular/app_modules.ts b/x-pack/plugins/monitoring/public/angular/app_modules.ts
index f3d77b196b26e..499610045d771 100644
--- a/x-pack/plugins/monitoring/public/angular/app_modules.ts
+++ b/x-pack/plugins/monitoring/public/angular/app_modules.ts
@@ -23,7 +23,7 @@ import { GlobalState } from '../url_state';
import { getSafeForExternalLink } from '../lib/get_safe_for_external_link';
// @ts-ignore
-import { formatNumber, formatMetric } from '../lib/format_number';
+import { formatMetric, formatNumber } from '../lib/format_number';
// @ts-ignore
import { extractIp } from '../lib/extract_ip';
// @ts-ignore
@@ -65,7 +65,7 @@ export const localAppModule = ({
createLocalPrivateModule();
createLocalStorage();
createLocalConfigModule(core);
- createLocalStateModule(query);
+ createLocalStateModule(query, core.notifications.toasts);
createLocalTopNavModule(navigation);
createHrefModule(core);
createMonitoringAppServices();
@@ -97,7 +97,10 @@ function createMonitoringAppConfigConstants(
keys.map(([key, value]) => (constantsModule = constantsModule.constant(key as string, value)));
}
-function createLocalStateModule(query: any) {
+function createLocalStateModule(
+ query: MonitoringStartPluginDependencies['data']['query'],
+ toasts: MonitoringStartPluginDependencies['core']['notifications']['toasts']
+) {
angular
.module('monitoring/State', ['monitoring/Private'])
.service('globalState', function (
@@ -106,7 +109,7 @@ function createLocalStateModule(query: any) {
$location: ng.ILocationService
) {
function GlobalStateProvider(this: any) {
- const state = new GlobalState(query, $rootScope, $location, this);
+ const state = new GlobalState(query, toasts, $rootScope, $location, this);
const initialState: any = state.getState();
for (const key in initialState) {
if (!initialState.hasOwnProperty(key)) {
diff --git a/x-pack/plugins/monitoring/public/url_state.ts b/x-pack/plugins/monitoring/public/url_state.ts
index e53497d751f9b..65e48223d7a64 100644
--- a/x-pack/plugins/monitoring/public/url_state.ts
+++ b/x-pack/plugins/monitoring/public/url_state.ts
@@ -23,6 +23,7 @@ import {
IKbnUrlStateStorage,
ISyncStateRef,
syncState,
+ withNotifyOnErrors,
} from '../../../../src/plugins/kibana_utils/public';
interface Route {
@@ -71,6 +72,7 @@ export class GlobalState {
constructor(
queryService: MonitoringStartPluginDependencies['data']['query'],
+ toasts: MonitoringStartPluginDependencies['core']['notifications']['toasts'],
rootScope: ng.IRootScopeService,
ngLocation: ng.ILocationService,
externalState: RawObject
@@ -78,7 +80,11 @@ export class GlobalState {
this.timefilterRef = queryService.timefilter.timefilter;
const history: History = createHashHistory();
- this.stateStorage = createKbnUrlStateStorage({ useHash: false, history });
+ this.stateStorage = createKbnUrlStateStorage({
+ useHash: false,
+ history,
+ ...withNotifyOnErrors(toasts),
+ });
const initialStateFromUrl = this.stateStorage.get(GLOBAL_STATE_KEY) as MonitoringAppState;
diff --git a/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_from_request.js b/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_from_request.js
index 18db738bba38e..16d42d896ca11 100644
--- a/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_from_request.js
+++ b/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_from_request.js
@@ -119,65 +119,67 @@ export async function getClustersFromRequest(
// add alerts data
if (isInCodePath(codePaths, [CODE_PATH_ALERTS])) {
const alertsClient = req.getAlertsClient();
- for (const cluster of clusters) {
- const verification = verifyMonitoringLicense(req.server);
- if (!verification.enabled) {
- // return metadata detailing that alerts is disabled because of the monitoring cluster license
- cluster.alerts = {
- alertsMeta: {
- enabled: verification.enabled,
- message: verification.message, // NOTE: this is only defined when the alert feature is disabled
- },
- list: {},
- };
- continue;
- }
+ if (alertsClient) {
+ for (const cluster of clusters) {
+ const verification = verifyMonitoringLicense(req.server);
+ if (!verification.enabled) {
+ // return metadata detailing that alerts is disabled because of the monitoring cluster license
+ cluster.alerts = {
+ alertsMeta: {
+ enabled: verification.enabled,
+ message: verification.message, // NOTE: this is only defined when the alert feature is disabled
+ },
+ list: {},
+ };
+ continue;
+ }
+
+ // check the license type of the production cluster for alerts feature support
+ const license = cluster.license || {};
+ const prodLicenseInfo = checkLicenseForAlerts(
+ license.type,
+ license.status === 'active',
+ 'production'
+ );
+ if (prodLicenseInfo.clusterAlerts.enabled) {
+ cluster.alerts = {
+ list: await fetchStatus(
+ alertsClient,
+ req.server.plugins.monitoring.info,
+ undefined,
+ cluster.cluster_uuid,
+ start,
+ end,
+ []
+ ),
+ alertsMeta: {
+ enabled: true,
+ },
+ };
+ continue;
+ }
- // check the license type of the production cluster for alerts feature support
- const license = cluster.license || {};
- const prodLicenseInfo = checkLicenseForAlerts(
- license.type,
- license.status === 'active',
- 'production'
- );
- if (prodLicenseInfo.clusterAlerts.enabled) {
cluster.alerts = {
- list: await fetchStatus(
- alertsClient,
- req.server.plugins.monitoring.info,
- undefined,
- cluster.cluster_uuid,
- start,
- end,
- []
- ),
+ list: {},
alertsMeta: {
enabled: true,
},
+ clusterMeta: {
+ enabled: false,
+ message: i18n.translate(
+ 'xpack.monitoring.clusterAlerts.unsupportedClusterAlertsDescription',
+ {
+ defaultMessage:
+ 'Cluster [{clusterName}] license type [{licenseType}] does not support Cluster Alerts',
+ values: {
+ clusterName: cluster.cluster_name,
+ licenseType: `${license.type}`,
+ },
+ }
+ ),
+ },
};
- continue;
}
-
- cluster.alerts = {
- list: {},
- alertsMeta: {
- enabled: true,
- },
- clusterMeta: {
- enabled: false,
- message: i18n.translate(
- 'xpack.monitoring.clusterAlerts.unsupportedClusterAlertsDescription',
- {
- defaultMessage:
- 'Cluster [{clusterName}] license type [{licenseType}] does not support Cluster Alerts',
- values: {
- clusterName: cluster.cluster_name,
- licenseType: `${license.type}`,
- },
- }
- ),
- },
- };
}
}
}
diff --git a/x-pack/plugins/monitoring/server/plugin.ts b/x-pack/plugins/monitoring/server/plugin.ts
index ed091d4b8d7a7..3aedb6831e7ab 100644
--- a/x-pack/plugins/monitoring/server/plugin.ts
+++ b/x-pack/plugins/monitoring/server/plugin.ts
@@ -325,8 +325,22 @@ export class Plugin {
getKibanaStatsCollector: () => this.legacyShimDependencies.kibanaStatsCollector,
getUiSettingsService: () => context.core.uiSettings.client,
getActionTypeRegistry: () => context.actions?.listTypes(),
- getAlertsClient: () => plugins.alerts.getAlertsClientWithRequest(req),
- getActionsClient: () => plugins.actions.getActionsClientWithRequest(req),
+ getAlertsClient: () => {
+ try {
+ return plugins.alerts.getAlertsClientWithRequest(req);
+ } catch (err) {
+ // If security is disabled, this call will throw an error unless a certain config is set for dist builds
+ return null;
+ }
+ },
+ getActionsClient: () => {
+ try {
+ return plugins.actions.getActionsClientWithRequest(req);
+ } catch (err) {
+ // If security is disabled, this call will throw an error unless a certain config is set for dist builds
+ return null;
+ }
+ },
server: {
config: legacyConfigWrapper,
newPlatform: {
diff --git a/x-pack/plugins/observability/public/services/get_observability_alerts.ts b/x-pack/plugins/observability/public/services/get_observability_alerts.ts
index 602e4cf2bdd13..cff6726e47df9 100644
--- a/x-pack/plugins/observability/public/services/get_observability_alerts.ts
+++ b/x-pack/plugins/observability/public/services/get_observability_alerts.ts
@@ -11,15 +11,12 @@ const allowedConsumers = ['apm', 'uptime', 'logs', 'metrics', 'alerts'];
export async function getObservabilityAlerts({ core }: { core: AppMountContext['core'] }) {
try {
- const { data = [] }: { data: Alert[] } = await core.http.get(
- core.http.basePath.prepend('/api/alerts/_find'),
- {
- query: {
- page: 1,
- per_page: 20,
- },
- }
- );
+ const { data = [] }: { data: Alert[] } = await core.http.get('/api/alerts/_find', {
+ query: {
+ page: 1,
+ per_page: 20,
+ },
+ });
return data.filter(({ consumer }) => allowedConsumers.includes(consumer));
} catch (e) {
diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts
index c74cf888a2db6..0fc42895050a5 100644
--- a/x-pack/plugins/security_solution/common/constants.ts
+++ b/x-pack/plugins/security_solution/common/constants.ts
@@ -140,6 +140,13 @@ export const UNAUTHENTICATED_USER = 'Unauthenticated';
*/
export const MINIMUM_ML_LICENSE = 'platinum';
+/*
+ Machine Learning constants
+ */
+export const ML_GROUP_ID = 'security';
+export const LEGACY_ML_GROUP_ID = 'siem';
+export const ML_GROUP_IDS = [ML_GROUP_ID, LEGACY_ML_GROUP_ID];
+
/*
Rule notifications options
*/
diff --git a/x-pack/plugins/security_solution/common/endpoint/models/ecs_safety_helpers.ts b/x-pack/plugins/security_solution/common/endpoint/models/ecs_safety_helpers.ts
new file mode 100644
index 0000000000000..8b419e90a6ee9
--- /dev/null
+++ b/x-pack/plugins/security_solution/common/endpoint/models/ecs_safety_helpers.ts
@@ -0,0 +1,61 @@
+/*
+ * 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 { ECSField } from '../types';
+
+/**
+ * Use these functions to accecss information held in `ECSField`s.
+ */
+
+/**
+ * True if the field contains `expected`. If the field contains an array, this will be true if the array contains `expected`.
+ */
+export function hasValue(valueOrCollection: ECSField, expected: T): boolean {
+ if (Array.isArray(valueOrCollection)) {
+ return valueOrCollection.includes(expected);
+ } else {
+ return valueOrCollection === expected;
+ }
+}
+
+/**
+ * Return first non-null value. If the field contains an array, this will return the first value that isn't null. If the field isn't an array it'll be returned unless it's null.
+ */
+export function firstNonNullValue(valueOrCollection: ECSField): T | undefined {
+ if (valueOrCollection === null) {
+ return undefined;
+ } else if (Array.isArray(valueOrCollection)) {
+ for (const value of valueOrCollection) {
+ if (value !== null) {
+ return value;
+ }
+ }
+ } else {
+ return valueOrCollection;
+ }
+}
+
+/*
+ * Get an array of all non-null values. If there is just 1 value, return it wrapped in an array. If there are multiple values, return the non-null ones.
+ * Use this when you want to consistently access the value(s) as an array.
+ */
+export function values(valueOrCollection: ECSField): T[] {
+ if (Array.isArray(valueOrCollection)) {
+ const nonNullValues: T[] = [];
+ for (const value of valueOrCollection) {
+ if (value !== null) {
+ nonNullValues.push(value);
+ }
+ }
+ return nonNullValues;
+ } else if (valueOrCollection !== null) {
+ // if there is a single non-null value, wrap it in an array and return it.
+ return [valueOrCollection];
+ } else {
+ // if the value was null, return `[]`.
+ return [];
+ }
+}
diff --git a/x-pack/plugins/security_solution/common/endpoint/models/event.ts b/x-pack/plugins/security_solution/common/endpoint/models/event.ts
index 1168b5edb6ffd..b1a8524a9f9e7 100644
--- a/x-pack/plugins/security_solution/common/endpoint/models/event.ts
+++ b/x-pack/plugins/security_solution/common/endpoint/models/event.ts
@@ -3,8 +3,26 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-import { LegacyEndpointEvent, ResolverEvent } from '../types';
+import {
+ LegacyEndpointEvent,
+ ResolverEvent,
+ SafeResolverEvent,
+ SafeLegacyEndpointEvent,
+} from '../types';
+import { firstNonNullValue } from './ecs_safety_helpers';
+/*
+ * Determine if a `ResolverEvent` is the legacy variety. Can be used to narrow `ResolverEvent` to `LegacyEndpointEvent`.
+ */
+export function isLegacyEventSafeVersion(
+ event: SafeResolverEvent
+): event is SafeLegacyEndpointEvent {
+ return 'endgame' in event && event.endgame !== undefined;
+}
+
+/*
+ * Determine if a `ResolverEvent` is the legacy variety. Can be used to narrow `ResolverEvent` to `LegacyEndpointEvent`. See `isLegacyEventSafeVersion`
+ */
export function isLegacyEvent(event: ResolverEvent): event is LegacyEndpointEvent {
return (event as LegacyEndpointEvent).endgame !== undefined;
}
@@ -31,6 +49,12 @@ export function isProcessRunning(event: ResolverEvent): boolean {
);
}
+export function timestampSafeVersion(event: SafeResolverEvent): string | undefined | number {
+ return isLegacyEventSafeVersion(event)
+ ? firstNonNullValue(event.endgame?.timestamp_utc)
+ : firstNonNullValue(event?.['@timestamp']);
+}
+
export function eventTimestamp(event: ResolverEvent): string | undefined | number {
if (isLegacyEvent(event)) {
return event.endgame.timestamp_utc;
@@ -47,6 +71,14 @@ export function eventName(event: ResolverEvent): string {
}
}
+export function processNameSafeVersion(event: SafeResolverEvent): string | undefined {
+ if (isLegacyEventSafeVersion(event)) {
+ return firstNonNullValue(event.endgame.process_name);
+ } else {
+ return firstNonNullValue(event.process?.name);
+ }
+}
+
export function eventId(event: ResolverEvent): number | undefined | string {
if (isLegacyEvent(event)) {
return event.endgame.serial_event_id;
@@ -54,6 +86,12 @@ export function eventId(event: ResolverEvent): number | undefined | string {
return event.event.id;
}
+export function eventIDSafeVersion(event: SafeResolverEvent): number | undefined | string {
+ return firstNonNullValue(
+ isLegacyEventSafeVersion(event) ? event.endgame?.serial_event_id : event.event?.id
+ );
+}
+
export function entityId(event: ResolverEvent): string {
if (isLegacyEvent(event)) {
return event.endgame.unique_pid ? String(event.endgame.unique_pid) : '';
@@ -61,6 +99,16 @@ export function entityId(event: ResolverEvent): string {
return event.process.entity_id;
}
+export function entityIDSafeVersion(event: SafeResolverEvent): string | undefined {
+ if (isLegacyEventSafeVersion(event)) {
+ return event.endgame?.unique_pid === undefined
+ ? undefined
+ : String(firstNonNullValue(event.endgame.unique_pid));
+ } else {
+ return firstNonNullValue(event.process?.entity_id);
+ }
+}
+
export function parentEntityId(event: ResolverEvent): string | undefined {
if (isLegacyEvent(event)) {
return event.endgame.unique_ppid ? String(event.endgame.unique_ppid) : undefined;
@@ -68,6 +116,13 @@ export function parentEntityId(event: ResolverEvent): string | undefined {
return event.process.parent?.entity_id;
}
+export function parentEntityIDSafeVersion(event: SafeResolverEvent): string | undefined {
+ if (isLegacyEventSafeVersion(event)) {
+ return String(firstNonNullValue(event.endgame.unique_ppid));
+ }
+ return firstNonNullValue(event.process?.parent?.entity_id);
+}
+
export function ancestryArray(event: ResolverEvent): string[] | undefined {
if (isLegacyEvent(event)) {
return undefined;
diff --git a/x-pack/plugins/security_solution/common/endpoint/types.ts b/x-pack/plugins/security_solution/common/endpoint/types.ts
index 1c24e1abe5a57..61ce672405fd5 100644
--- a/x-pack/plugins/security_solution/common/endpoint/types.ts
+++ b/x-pack/plugins/security_solution/common/endpoint/types.ts
@@ -508,6 +508,8 @@ export interface EndpointEvent {
ecs: {
version: string;
};
+ // A legacy has `endgame` and an `EndpointEvent` (AKA ECS event) will never have it. This helps TS narrow `SafeResolverEvent`.
+ endgame?: never;
event: {
category: string | string[];
type: string | string[];
@@ -559,6 +561,130 @@ export interface EndpointEvent {
export type ResolverEvent = EndpointEvent | LegacyEndpointEvent;
+/**
+ * All mappings in Elasticsearch support arrays. They can also return null values or be missing. For example, a `keyword` mapping could return `null` or `[null]` or `[]` or `'hi'`, or `['hi', 'there']`. We need to handle these cases in order to avoid throwing an error.
+ * When dealing with an value that comes from ES, wrap the underlying type in `ECSField`. For example, if you have a `keyword` or `text` value coming from ES, cast it to `ECSField`.
+ */
+export type ECSField = T | null | Array;
+
+/**
+ * A more conservative version of `ResolverEvent` that treats fields as optional and use `ECSField` to type all ECS fields.
+ * Prefer this over `ResolverEvent`.
+ */
+export type SafeResolverEvent = SafeEndpointEvent | SafeLegacyEndpointEvent;
+
+/**
+ * Safer version of ResolverEvent. Please use this going forward.
+ */
+export type SafeEndpointEvent = Partial<{
+ '@timestamp': ECSField;
+ agent: Partial<{
+ id: ECSField;
+ version: ECSField;
+ type: ECSField;
+ }>;
+ ecs: Partial<{
+ version: ECSField;
+ }>;
+ event: Partial<{
+ category: ECSField;
+ type: ECSField;
+ id: ECSField;
+ kind: ECSField;
+ }>;
+ host: Partial<{
+ id: ECSField;
+ hostname: ECSField;
+ name: ECSField;
+ ip: ECSField;
+ mac: ECSField;
+ architecture: ECSField;
+ os: Partial<{
+ full: ECSField;
+ name: ECSField;
+ version: ECSField;
+ platform: ECSField;
+ family: ECSField;
+ Ext: Partial<{
+ variant: ECSField;
+ }>;
+ }>;
+ }>;
+ network: Partial<{
+ direction: ECSField;
+ forwarded_ip: ECSField;
+ }>;
+ dns: Partial<{
+ question: Partial<{ name: ECSField }>;
+ }>;
+ process: Partial<{
+ entity_id: ECSField;
+ name: ECSField;
+ executable: ECSField;
+ args: ECSField;
+ code_signature: Partial<{
+ status: ECSField;
+ subject_name: ECSField;
+ }>;
+ pid: ECSField;
+ hash: Partial<{
+ md5: ECSField;
+ }>;
+ parent: Partial<{
+ entity_id: ECSField;
+ name: ECSField;
+ pid: ECSField;
+ }>;
+ /*
+ * The array has a special format. The entity_ids towards the beginning of the array are closer ancestors and the
+ * values towards the end of the array are more distant ancestors (grandparents). Therefore
+ * ancestry_array[0] == process.parent.entity_id and ancestry_array[1] == process.parent.parent.entity_id
+ */
+ Ext: Partial<{
+ ancestry: ECSField;
+ }>;
+ }>;
+ user: Partial<{
+ domain: ECSField;
+ name: ECSField;
+ }>;
+ file: Partial<{ path: ECSField }>;
+ registry: Partial<{ path: ECSField; key: ECSField }>;
+}>;
+
+export interface SafeLegacyEndpointEvent {
+ '@timestamp'?: ECSField;
+ /**
+ * 'legacy' events must have an `endgame` key.
+ */
+ endgame: Partial<{
+ pid: ECSField;
+ ppid: ECSField;
+ event_type_full: ECSField;
+ event_subtype_full: ECSField;
+ event_timestamp: ECSField;
+ event_type: ECSField;
+ unique_pid: ECSField;
+ unique_ppid: ECSField;
+ machine_id: ECSField;
+ process_name: ECSField;
+ process_path: ECSField;
+ timestamp_utc: ECSField;
+ serial_event_id: ECSField;
+ }>;
+ agent: Partial<{
+ id: ECSField;
+ type: ECSField;
+ version: ECSField;
+ }>;
+ event: Partial<{
+ action: ECSField;
+ type: ECSField;
+ category: ECSField;
+ id: ECSField;
+ }>;
+}
+
/**
* The response body for the resolver '/entity' index API
*/
diff --git a/x-pack/plugins/security_solution/common/machine_learning/is_security_job.test.ts b/x-pack/plugins/security_solution/common/machine_learning/is_security_job.test.ts
new file mode 100644
index 0000000000000..abb0c790584af
--- /dev/null
+++ b/x-pack/plugins/security_solution/common/machine_learning/is_security_job.test.ts
@@ -0,0 +1,30 @@
+/*
+ * 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 { MlSummaryJob } from '../../../ml/common/types/anomaly_detection_jobs';
+import { isSecurityJob } from './is_security_job';
+
+describe('isSecurityJob', () => {
+ it('counts a job with a group of "siem"', () => {
+ const job = { groups: ['siem', 'other'] } as MlSummaryJob;
+ expect(isSecurityJob(job)).toEqual(true);
+ });
+
+ it('counts a job with a group of "security"', () => {
+ const job = { groups: ['security', 'other'] } as MlSummaryJob;
+ expect(isSecurityJob(job)).toEqual(true);
+ });
+
+ it('counts a job in both "security" and "siem"', () => {
+ const job = { groups: ['siem', 'security'] } as MlSummaryJob;
+ expect(isSecurityJob(job)).toEqual(true);
+ });
+
+ it('does not count a job in a related group', () => {
+ const job = { groups: ['auditbeat', 'process'] } as MlSummaryJob;
+ expect(isSecurityJob(job)).toEqual(false);
+ });
+});
diff --git a/x-pack/plugins/security_solution/common/machine_learning/is_security_job.ts b/x-pack/plugins/security_solution/common/machine_learning/is_security_job.ts
new file mode 100644
index 0000000000000..43cfa4ad59964
--- /dev/null
+++ b/x-pack/plugins/security_solution/common/machine_learning/is_security_job.ts
@@ -0,0 +1,11 @@
+/*
+ * 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 { MlSummaryJob } from '../../../ml/common/types/anomaly_detection_jobs';
+import { ML_GROUP_IDS } from '../constants';
+
+export const isSecurityJob = (job: MlSummaryJob): boolean =>
+ job.groups.some((group) => ML_GROUP_IDS.includes(group));
diff --git a/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.ts b/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.ts
index 8c7acfc18ece6..c4702e915c076 100644
--- a/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.ts
+++ b/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.ts
@@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import deepEqual from 'fast-deep-equal';
import { isEmpty } from 'lodash/fp';
import { useEffect, useMemo, useState, useRef } from 'react';
@@ -34,7 +35,6 @@ export const useQuery = ({
}
return configIndex;
}, [configIndex, indexToAdd]);
-
const [, dispatchToaster] = useStateToaster();
const refetch = useRef();
const [loading, setLoading] = useState(false);
@@ -43,20 +43,54 @@ export const useQuery = ({
const [totalCount, setTotalCount] = useState(-1);
const apolloClient = useApolloClient();
+ const [matrixHistogramVariables, setMatrixHistogramVariables] = useState<
+ GetMatrixHistogramQuery.Variables
+ >({
+ filterQuery: createFilter(filterQuery),
+ sourceId: 'default',
+ timerange: {
+ interval: '12h',
+ from: startDate!,
+ to: endDate!,
+ },
+ defaultIndex,
+ inspect: isInspected,
+ stackByField,
+ histogramType,
+ });
+
+ useEffect(() => {
+ setMatrixHistogramVariables((prevVariables) => {
+ const localVariables = {
+ filterQuery: createFilter(filterQuery),
+ sourceId: 'default',
+ timerange: {
+ interval: '12h',
+ from: startDate!,
+ to: endDate!,
+ },
+ defaultIndex,
+ inspect: isInspected,
+ stackByField,
+ histogramType,
+ };
+ if (!deepEqual(prevVariables, localVariables)) {
+ return localVariables;
+ }
+ return prevVariables;
+ });
+ }, [
+ defaultIndex,
+ filterQuery,
+ histogramType,
+ indexToAdd,
+ isInspected,
+ stackByField,
+ startDate,
+ endDate,
+ ]);
+
useEffect(() => {
- const matrixHistogramVariables: GetMatrixHistogramQuery.Variables = {
- filterQuery: createFilter(filterQuery),
- sourceId: 'default',
- timerange: {
- interval: '12h',
- from: startDate!,
- to: endDate!,
- },
- defaultIndex,
- inspect: isInspected,
- stackByField,
- histogramType,
- };
let isSubscribed = true;
const abortCtrl = new AbortController();
const abortSignal = abortCtrl.signal;
@@ -102,19 +136,7 @@ export const useQuery = ({
isSubscribed = false;
abortCtrl.abort();
};
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [
- defaultIndex,
- errorMessage,
- filterQuery,
- histogramType,
- indexToAdd,
- isInspected,
- stackByField,
- startDate,
- endDate,
- data,
- ]);
+ }, [apolloClient, dispatchToaster, errorMessage, matrixHistogramVariables]);
return { data, loading, inspect, totalCount, refetch: refetch.current };
};
diff --git a/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts b/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts
index a415ab75f13ea..ab9f12a67fe89 100644
--- a/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts
+++ b/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts
@@ -8,7 +8,13 @@ import { FilterStateStore } from '../../../../../../src/plugins/data/common/es_q
import { TimelineType, TimelineStatus } from '../../../common/types/timeline';
import { OpenTimelineResult } from '../../timelines/components/open_timeline/types';
-import { GetAllTimeline, SortFieldTimeline, TimelineResult, Direction } from '../../graphql/types';
+import {
+ GetAllTimeline,
+ SortFieldTimeline,
+ TimelineResult,
+ Direction,
+ DetailItem,
+} from '../../graphql/types';
import { allTimelinesQuery } from '../../timelines/containers/all/index.gql_query';
import { CreateTimelineProps } from '../../detections/components/alerts_table/types';
import { TimelineModel } from '../../timelines/store/timeline/model';
@@ -2252,5 +2258,32 @@ export const defaultTimelineProps: CreateTimelineProps = {
width: 1100,
},
to: '2018-11-05T19:03:25.937Z',
+ notes: null,
ruleNote: '# this is some markdown documentation',
};
+
+export const mockTimelineDetails: DetailItem[] = [
+ {
+ field: 'host.name',
+ values: ['apache'],
+ originalValue: 'apache',
+ },
+ {
+ field: 'user.id',
+ values: ['1'],
+ originalValue: 1,
+ },
+];
+
+export const mockTimelineDetailsApollo = {
+ data: {
+ source: {
+ TimelineDetails: {
+ data: mockTimelineDetails,
+ },
+ },
+ },
+ loading: false,
+ networkStatus: 7,
+ stale: false,
+};
diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx
index c2b51e29c230d..e8015f601cb18 100644
--- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx
@@ -3,6 +3,8 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
+
+import { get } from 'lodash/fp';
import sinon from 'sinon';
import moment from 'moment';
@@ -12,6 +14,7 @@ import {
defaultTimelineProps,
apolloClient,
mockTimelineApolloResult,
+ mockTimelineDetailsApollo,
} from '../../../common/mock/';
import { CreateTimeline, UpdateTimelineLoading } from './types';
import { Ecs } from '../../../graphql/types';
@@ -37,7 +40,13 @@ describe('alert actions', () => {
createTimeline = jest.fn() as jest.Mocked;
updateTimelineIsLoading = jest.fn() as jest.Mocked;
- jest.spyOn(apolloClient, 'query').mockResolvedValue(mockTimelineApolloResult);
+ jest.spyOn(apolloClient, 'query').mockImplementation((obj) => {
+ const id = get('variables.id', obj);
+ if (id != null) {
+ return Promise.resolve(mockTimelineApolloResult);
+ }
+ return Promise.resolve(mockTimelineDetailsApollo);
+ });
clock = sinon.useFakeTimers(unix);
});
@@ -71,6 +80,7 @@ describe('alert actions', () => {
});
const expected = {
from: '2018-11-05T18:58:25.937Z',
+ notes: null,
timeline: {
columns: [
{
diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx
index 7bebc9efbee15..34c0537a6d7d2 100644
--- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx
@@ -19,6 +19,8 @@ import {
Ecs,
TimelineStatus,
TimelineType,
+ GetTimelineDetailsQuery,
+ DetailItem,
} from '../../../graphql/types';
import { oneTimelineQuery } from '../../../timelines/containers/one/index.gql_query';
import { timelineDefaults } from '../../../timelines/store/timeline/defaults';
@@ -34,6 +36,7 @@ import {
} from './helpers';
import { KueryFilterQueryKind } from '../../../common/store';
import { DataProvider } from '../../../timelines/components/timeline/data_providers/data_provider';
+import { timelineDetailsQuery } from '../../../timelines/containers/details/index.gql_query';
export const getUpdateAlertsQuery = (eventIds: Readonly) => {
return {
@@ -153,35 +156,45 @@ export const sendAlertToTimelineAction = async ({
if (timelineId !== '' && apolloClient != null) {
try {
updateTimelineIsLoading({ id: 'timeline-1', isLoading: true });
- const responseTimeline = await apolloClient.query<
- GetOneTimeline.Query,
- GetOneTimeline.Variables
- >({
- query: oneTimelineQuery,
- fetchPolicy: 'no-cache',
- variables: {
- id: timelineId,
- },
- });
+ const [responseTimeline, eventDataResp] = await Promise.all([
+ apolloClient.query({
+ query: oneTimelineQuery,
+ fetchPolicy: 'no-cache',
+ variables: {
+ id: timelineId,
+ },
+ }),
+ apolloClient.query({
+ query: timelineDetailsQuery,
+ fetchPolicy: 'no-cache',
+ variables: {
+ defaultIndex: [],
+ docValueFields: [],
+ eventId: ecsData._id,
+ indexName: ecsData._index ?? '',
+ sourceId: 'default',
+ },
+ }),
+ ]);
const resultingTimeline: TimelineResult = getOr({}, 'data.getOneTimeline', responseTimeline);
-
+ const eventData: DetailItem[] = getOr([], 'data.source.TimelineDetails.data', eventDataResp);
if (!isEmpty(resultingTimeline)) {
const timelineTemplate: TimelineResult = omitTypenameInTimeline(resultingTimeline);
openAlertInBasicTimeline = false;
- const { timeline } = formatTimelineResultToModel(
+ const { timeline, notes } = formatTimelineResultToModel(
timelineTemplate,
true,
timelineTemplate.timelineType ?? TimelineType.default
);
const query = replaceTemplateFieldFromQuery(
timeline.kqlQuery?.filterQuery?.kuery?.expression ?? '',
- ecsData,
+ eventData,
timeline.timelineType
);
- const filters = replaceTemplateFieldFromMatchFilters(timeline.filters ?? [], ecsData);
+ const filters = replaceTemplateFieldFromMatchFilters(timeline.filters ?? [], eventData);
const dataProviders = replaceTemplateFieldFromDataProviders(
timeline.dataProviders ?? [],
- ecsData,
+ eventData,
timeline.timelineType
);
@@ -213,10 +226,12 @@ export const sendAlertToTimelineAction = async ({
expression: query,
},
},
+ noteIds: notes?.map((n) => n.noteId) ?? [],
show: true,
},
to,
ruleNote: noteContent,
+ notes: notes ?? null,
});
}
} catch {
@@ -232,6 +247,7 @@ export const sendAlertToTimelineAction = async ({
) {
return createTimeline({
from,
+ notes: null,
timeline: {
...timelineDefaults,
dataProviders: [
@@ -282,6 +298,7 @@ export const sendAlertToTimelineAction = async ({
} else {
return createTimeline({
from,
+ notes: null,
timeline: {
...timelineDefaults,
dataProviders: [
diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/helpers.test.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_table/helpers.test.ts
index 4decddd6b8886..7ac254f2e84f7 100644
--- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/helpers.test.ts
+++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/helpers.test.ts
@@ -3,10 +3,8 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-import { cloneDeep } from 'lodash/fp';
import { TimelineType } from '../../../../common/types/timeline';
-import { mockEcsData } from '../../../common/mock/mock_ecs';
import { Filter } from '../../../../../../../src/plugins/data/public';
import {
DataProvider,
@@ -20,31 +18,40 @@ import {
replaceTemplateFieldFromMatchFilters,
reformatDataProviderWithNewValue,
} from './helpers';
+import { mockTimelineDetails } from '../../../common/mock';
describe('helpers', () => {
- let mockEcsDataClone = cloneDeep(mockEcsData);
- beforeEach(() => {
- mockEcsDataClone = cloneDeep(mockEcsData);
- });
describe('getStringOrStringArray', () => {
test('it should correctly return a string array', () => {
- const value = getStringArray('x', {
- x: 'The nickname of the developer we all :heart:',
- });
+ const value = getStringArray('x', [
+ {
+ field: 'x',
+ values: ['The nickname of the developer we all :heart:'],
+ originalValue: 'The nickname of the developer we all :heart:',
+ },
+ ]);
expect(value).toEqual(['The nickname of the developer we all :heart:']);
});
test('it should correctly return a string array with a single element', () => {
- const value = getStringArray('x', {
- x: ['The nickname of the developer we all :heart:'],
- });
+ const value = getStringArray('x', [
+ {
+ field: 'x',
+ values: ['The nickname of the developer we all :heart:'],
+ originalValue: 'The nickname of the developer we all :heart:',
+ },
+ ]);
expect(value).toEqual(['The nickname of the developer we all :heart:']);
});
test('it should correctly return a string array with two elements of strings', () => {
- const value = getStringArray('x', {
- x: ['The nickname of the developer we all :heart:', 'We are all made of stars'],
- });
+ const value = getStringArray('x', [
+ {
+ field: 'x',
+ values: ['The nickname of the developer we all :heart:', 'We are all made of stars'],
+ originalValue: 'The nickname of the developer we all :heart:',
+ },
+ ]);
expect(value).toEqual([
'The nickname of the developer we all :heart:',
'We are all made of stars',
@@ -52,22 +59,40 @@ describe('helpers', () => {
});
test('it should correctly return a string array with deep elements', () => {
- const value = getStringArray('x.y.z', {
- x: { y: { z: 'zed' } },
- });
+ const value = getStringArray('x.y.z', [
+ {
+ field: 'x.y.z',
+ values: ['zed'],
+ originalValue: 'zed',
+ },
+ ]);
expect(value).toEqual(['zed']);
});
test('it should correctly return a string array with a non-existent value', () => {
- const value = getStringArray('non.existent', {
- x: { y: { z: 'zed' } },
- });
+ const value = getStringArray('non.existent', [
+ {
+ field: 'x.y.z',
+ values: ['zed'],
+ originalValue: 'zed',
+ },
+ ]);
expect(value).toEqual([]);
});
test('it should trace an error if the value is not a string', () => {
const mockConsole: Console = ({ trace: jest.fn() } as unknown) as Console;
- const value = getStringArray('a', { a: 5 }, mockConsole);
+ const value = getStringArray(
+ 'a',
+ [
+ {
+ field: 'a',
+ values: (5 as unknown) as string[],
+ originalValue: 'zed',
+ },
+ ],
+ mockConsole
+ );
expect(value).toEqual([]);
expect(
mockConsole.trace
@@ -77,13 +102,23 @@ describe('helpers', () => {
'when trying to access field:',
'a',
'from data object of:',
- { a: 5 }
+ [{ field: 'a', originalValue: 'zed', values: 5 }]
);
});
test('it should trace an error if the value is an array of mixed values', () => {
const mockConsole: Console = ({ trace: jest.fn() } as unknown) as Console;
- const value = getStringArray('a', { a: ['hi', 5] }, mockConsole);
+ const value = getStringArray(
+ 'a',
+ [
+ {
+ field: 'a',
+ values: (['hi', 5] as unknown) as string[],
+ originalValue: 'zed',
+ },
+ ],
+ mockConsole
+ );
expect(value).toEqual([]);
expect(
mockConsole.trace
@@ -93,7 +128,7 @@ describe('helpers', () => {
'when trying to access field:',
'a',
'from data object of:',
- { a: ['hi', 5] }
+ [{ field: 'a', originalValue: 'zed', values: ['hi', 5] }]
);
});
});
@@ -103,7 +138,7 @@ describe('helpers', () => {
test('given an empty query string this returns an empty query string', () => {
const replacement = replaceTemplateFieldFromQuery(
'',
- mockEcsDataClone[0],
+ mockTimelineDetails,
TimelineType.default
);
expect(replacement).toEqual('');
@@ -112,7 +147,7 @@ describe('helpers', () => {
test('given a query string with spaces this returns an empty query string', () => {
const replacement = replaceTemplateFieldFromQuery(
' ',
- mockEcsDataClone[0],
+ mockTimelineDetails,
TimelineType.default
);
expect(replacement).toEqual('');
@@ -121,17 +156,21 @@ describe('helpers', () => {
test('it should replace a query with a template value such as apache from a mock template', () => {
const replacement = replaceTemplateFieldFromQuery(
'host.name: placeholdertext',
- mockEcsDataClone[0],
+ mockTimelineDetails,
TimelineType.default
);
expect(replacement).toEqual('host.name: apache');
});
test('it should replace a template field with an ECS value that is not an array', () => {
- mockEcsDataClone[0].host!.name = ('apache' as unknown) as string[]; // very unsafe cast for this test case
+ const dupTimelineDetails = [...mockTimelineDetails];
+ dupTimelineDetails[0] = {
+ ...dupTimelineDetails[0],
+ values: ('apache' as unknown) as string[],
+ }; // very unsafe cast for this test case
const replacement = replaceTemplateFieldFromQuery(
'host.name: *',
- mockEcsDataClone[0],
+ dupTimelineDetails,
TimelineType.default
);
expect(replacement).toEqual('host.name: *');
@@ -140,7 +179,7 @@ describe('helpers', () => {
test('it should NOT replace a query with a template value that is not part of the template fields array', () => {
const replacement = replaceTemplateFieldFromQuery(
'user.id: placeholdertext',
- mockEcsDataClone[0],
+ mockTimelineDetails,
TimelineType.default
);
expect(replacement).toEqual('user.id: placeholdertext');
@@ -151,7 +190,7 @@ describe('helpers', () => {
test('given an empty query string this returns an empty query string', () => {
const replacement = replaceTemplateFieldFromQuery(
'',
- mockEcsDataClone[0],
+ mockTimelineDetails,
TimelineType.template
);
expect(replacement).toEqual('');
@@ -160,7 +199,7 @@ describe('helpers', () => {
test('given a query string with spaces this returns an empty query string', () => {
const replacement = replaceTemplateFieldFromQuery(
' ',
- mockEcsDataClone[0],
+ mockTimelineDetails,
TimelineType.template
);
expect(replacement).toEqual('');
@@ -169,17 +208,21 @@ describe('helpers', () => {
test('it should NOT replace a query with a template value such as apache from a mock template', () => {
const replacement = replaceTemplateFieldFromQuery(
'host.name: placeholdertext',
- mockEcsDataClone[0],
+ mockTimelineDetails,
TimelineType.template
);
expect(replacement).toEqual('host.name: placeholdertext');
});
test('it should NOT replace a template field with an ECS value that is not an array', () => {
- mockEcsDataClone[0].host!.name = ('apache' as unknown) as string[]; // very unsafe cast for this test case
+ const dupTimelineDetails = [...mockTimelineDetails];
+ dupTimelineDetails[0] = {
+ ...dupTimelineDetails[0],
+ values: ('apache' as unknown) as string[],
+ }; // very unsafe cast for this test case
const replacement = replaceTemplateFieldFromQuery(
'host.name: *',
- mockEcsDataClone[0],
+ dupTimelineDetails,
TimelineType.default
);
expect(replacement).toEqual('host.name: *');
@@ -188,7 +231,7 @@ describe('helpers', () => {
test('it should NOT replace a query with a template value that is not part of the template fields array', () => {
const replacement = replaceTemplateFieldFromQuery(
'user.id: placeholdertext',
- mockEcsDataClone[0],
+ mockTimelineDetails,
TimelineType.default
);
expect(replacement).toEqual('user.id: placeholdertext');
@@ -198,7 +241,7 @@ describe('helpers', () => {
describe('replaceTemplateFieldFromMatchFilters', () => {
test('given an empty query filter this will return an empty filter', () => {
- const replacement = replaceTemplateFieldFromMatchFilters([], mockEcsDataClone[0]);
+ const replacement = replaceTemplateFieldFromMatchFilters([], mockTimelineDetails);
expect(replacement).toEqual([]);
});
@@ -216,7 +259,7 @@ describe('helpers', () => {
query: { match_phrase: { 'host.name': 'Braden' } },
},
];
- const replacement = replaceTemplateFieldFromMatchFilters(filters, mockEcsDataClone[0]);
+ const replacement = replaceTemplateFieldFromMatchFilters(filters, mockTimelineDetails);
const expected: Filter[] = [
{
meta: {
@@ -247,7 +290,7 @@ describe('helpers', () => {
query: { match_phrase: { 'user.id': 'Evan' } },
},
];
- const replacement = replaceTemplateFieldFromMatchFilters(filters, mockEcsDataClone[0]);
+ const replacement = replaceTemplateFieldFromMatchFilters(filters, mockTimelineDetails);
const expected: Filter[] = [
{
meta: {
@@ -275,7 +318,7 @@ describe('helpers', () => {
mockDataProvider.queryMatch.value = 'Braden';
const replacement = reformatDataProviderWithNewValue(
mockDataProvider,
- mockEcsDataClone[0],
+ mockTimelineDetails,
TimelineType.default
);
expect(replacement).toEqual({
@@ -297,7 +340,11 @@ describe('helpers', () => {
});
test('it should replace a query with a template value such as apache from a mock data provider using a string in the data provider', () => {
- mockEcsDataClone[0].host!.name = ('apache' as unknown) as string[]; // very unsafe cast for this test case
+ const dupTimelineDetails = [...mockTimelineDetails];
+ dupTimelineDetails[0] = {
+ ...dupTimelineDetails[0],
+ values: ('apache' as unknown) as string[],
+ }; // very unsafe cast for this test case
const mockDataProvider: DataProvider = mockDataProviders[0];
mockDataProvider.queryMatch.field = 'host.name';
mockDataProvider.id = 'Braden';
@@ -305,7 +352,7 @@ describe('helpers', () => {
mockDataProvider.queryMatch.value = 'Braden';
const replacement = reformatDataProviderWithNewValue(
mockDataProvider,
- mockEcsDataClone[0],
+ dupTimelineDetails,
TimelineType.default
);
expect(replacement).toEqual({
@@ -334,7 +381,7 @@ describe('helpers', () => {
mockDataProvider.queryMatch.value = 'Rebecca';
const replacement = reformatDataProviderWithNewValue(
mockDataProvider,
- mockEcsDataClone[0],
+ mockTimelineDetails,
TimelineType.default
);
expect(replacement).toEqual({
@@ -366,7 +413,7 @@ describe('helpers', () => {
mockDataProvider.type = DataProviderType.template;
const replacement = reformatDataProviderWithNewValue(
mockDataProvider,
- mockEcsDataClone[0],
+ mockTimelineDetails,
TimelineType.template
);
expect(replacement).toEqual({
@@ -396,7 +443,7 @@ describe('helpers', () => {
mockDataProvider.type = DataProviderType.default;
const replacement = reformatDataProviderWithNewValue(
mockDataProvider,
- mockEcsDataClone[0],
+ mockTimelineDetails,
TimelineType.template
);
expect(replacement).toEqual({
@@ -418,7 +465,11 @@ describe('helpers', () => {
});
test('it should replace a query with a template value such as apache from a mock data provider using a string in the data provider', () => {
- mockEcsDataClone[0].host!.name = ('apache' as unknown) as string[]; // very unsafe cast for this test case
+ const dupTimelineDetails = [...mockTimelineDetails];
+ dupTimelineDetails[0] = {
+ ...dupTimelineDetails[0],
+ values: ('apache' as unknown) as string[],
+ }; // very unsafe cast for this test case
const mockDataProvider: DataProvider = mockDataProviders[0];
mockDataProvider.queryMatch.field = 'host.name';
mockDataProvider.id = 'Braden';
@@ -427,7 +478,7 @@ describe('helpers', () => {
mockDataProvider.type = DataProviderType.template;
const replacement = reformatDataProviderWithNewValue(
mockDataProvider,
- mockEcsDataClone[0],
+ dupTimelineDetails,
TimelineType.template
);
expect(replacement).toEqual({
@@ -457,7 +508,7 @@ describe('helpers', () => {
mockDataProvider.type = DataProviderType.default;
const replacement = reformatDataProviderWithNewValue(
mockDataProvider,
- mockEcsDataClone[0],
+ mockTimelineDetails,
TimelineType.template
);
expect(replacement).toEqual({
diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/helpers.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_table/helpers.ts
index 084e4bff7e0ac..20c233a03a8cf 100644
--- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/helpers.ts
+++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/helpers.ts
@@ -4,14 +4,14 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { get, isEmpty } from 'lodash/fp';
+import { isEmpty } from 'lodash/fp';
import { Filter, esKuery, KueryNode } from '../../../../../../../src/plugins/data/public';
import {
DataProvider,
DataProviderType,
DataProvidersAnd,
} from '../../../timelines/components/timeline/data_providers/data_provider';
-import { Ecs, TimelineType } from '../../../graphql/types';
+import { DetailItem, TimelineType } from '../../../graphql/types';
interface FindValueToChangeInQuery {
field: string;
@@ -47,8 +47,12 @@ const templateFields = [
* @param data The unknown data that is typically a ECS value to get the value
* @param localConsole The local console which can be sent in to make this pure (for tests) or use the default console
*/
-export const getStringArray = (field: string, data: unknown, localConsole = console): string[] => {
- const value: unknown | undefined = get(field, data);
+export const getStringArray = (
+ field: string,
+ data: DetailItem[],
+ localConsole = console
+): string[] => {
+ const value: unknown | undefined = data.find((d) => d.field === field)?.values ?? null;
if (value == null) {
return [];
} else if (typeof value === 'string') {
@@ -104,14 +108,14 @@ export const findValueToChangeInQuery = (
export const replaceTemplateFieldFromQuery = (
query: string,
- ecsData: Ecs,
+ eventData: DetailItem[],
timelineType: TimelineType = TimelineType.default
): string => {
if (timelineType === TimelineType.default) {
if (query.trim() !== '') {
const valueToChange = findValueToChangeInQuery(esKuery.fromKueryExpression(query));
return valueToChange.reduce((newQuery, vtc) => {
- const newValue = getStringArray(vtc.field, ecsData);
+ const newValue = getStringArray(vtc.field, eventData);
if (newValue.length) {
return newQuery.replace(vtc.valueToChange, newValue[0]);
} else {
@@ -126,14 +130,17 @@ export const replaceTemplateFieldFromQuery = (
return query.trim();
};
-export const replaceTemplateFieldFromMatchFilters = (filters: Filter[], ecsData: Ecs): Filter[] =>
+export const replaceTemplateFieldFromMatchFilters = (
+ filters: Filter[],
+ eventData: DetailItem[]
+): Filter[] =>
filters.map((filter) => {
if (
filter.meta.type === 'phrase' &&
filter.meta.key != null &&
templateFields.includes(filter.meta.key)
) {
- const newValue = getStringArray(filter.meta.key, ecsData);
+ const newValue = getStringArray(filter.meta.key, eventData);
if (newValue.length) {
filter.meta.params = { query: newValue[0] };
filter.query = { match_phrase: { [filter.meta.key]: newValue[0] } };
@@ -144,13 +151,13 @@ export const replaceTemplateFieldFromMatchFilters = (filters: Filter[], ecsData:
export const reformatDataProviderWithNewValue = (
dataProvider: T,
- ecsData: Ecs,
+ eventData: DetailItem[],
timelineType: TimelineType = TimelineType.default
): T => {
// Support for legacy "template-like" timeline behavior that is using hardcoded list of templateFields
if (timelineType !== TimelineType.template) {
if (templateFields.includes(dataProvider.queryMatch.field)) {
- const newValue = getStringArray(dataProvider.queryMatch.field, ecsData);
+ const newValue = getStringArray(dataProvider.queryMatch.field, eventData);
if (newValue.length) {
dataProvider.id = dataProvider.id.replace(dataProvider.name, newValue[0]);
dataProvider.name = newValue[0];
@@ -168,7 +175,7 @@ export const reformatDataProviderWithNewValue =
dataProviders.map((dataProvider) => {
- const newDataProvider = reformatDataProviderWithNewValue(dataProvider, ecsData, timelineType);
+ const newDataProvider = reformatDataProviderWithNewValue(dataProvider, eventData, timelineType);
if (newDataProvider.and != null && !isEmpty(newDataProvider.and)) {
newDataProvider.and = newDataProvider.and.map((andDataProvider) =>
- reformatDataProviderWithNewValue(andDataProvider, ecsData, timelineType)
+ reformatDataProviderWithNewValue(andDataProvider, eventData, timelineType)
);
}
return newDataProvider;
diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx
index d93bad29f3348..66423259ec155 100644
--- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx
@@ -147,13 +147,14 @@ export const AlertsTableComponent: React.FC = ({
// Callback for creating a new timeline -- utilized by row/batch actions
const createTimelineCallback = useCallback(
- ({ from: fromTimeline, timeline, to: toTimeline, ruleNote }: CreateTimelineProps) => {
+ ({ from: fromTimeline, timeline, to: toTimeline, ruleNote, notes }: CreateTimelineProps) => {
updateTimelineIsLoading({ id: 'timeline-1', isLoading: false });
updateTimeline({
duplicate: true,
+ forceNotes: true,
from: fromTimeline,
id: 'timeline-1',
- notes: [],
+ notes,
timeline: {
...timeline,
show: true,
diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/types.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_table/types.ts
index ebf1a6d3ed533..2e77e77f6b3d5 100644
--- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/types.ts
+++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/types.ts
@@ -7,7 +7,7 @@
import ApolloClient from 'apollo-client';
import { Status } from '../../../../common/detection_engine/schemas/common/schemas';
-import { Ecs, TimelineNonEcsData } from '../../../graphql/types';
+import { Ecs, NoteResult, TimelineNonEcsData } from '../../../graphql/types';
import { TimelineModel } from '../../../timelines/store/timeline/model';
import { inputsModel } from '../../../common/store';
@@ -63,6 +63,7 @@ export interface CreateTimelineProps {
from: string;
timeline: TimelineModel;
to: string;
+ notes: NoteResult[] | null;
ruleNote?: string;
}
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/index.tsx
index b22ff406a1605..69dabeeb616a0 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/index.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/index.tsx
@@ -70,7 +70,12 @@ export const HostDetailsFlyout = memo(() => {
}, [error, toasts]);
return (
-
+
diff --git a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/one_ancestor_two_children.ts b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/one_ancestor_two_children.ts
index be0bc1b812a0b..94c176d343d17 100644
--- a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/one_ancestor_two_children.ts
+++ b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/one_ancestor_two_children.ts
@@ -10,7 +10,10 @@ import {
ResolverEntityIndex,
} from '../../../../common/endpoint/types';
import { mockEndpointEvent } from '../../store/mocks/endpoint_event';
-import { mockTreeWithNoAncestorsAnd2Children } from '../../store/mocks/resolver_tree';
+import {
+ mockTreeWithNoAncestorsAnd2Children,
+ withRelatedEventsOnOrigin,
+} from '../../store/mocks/resolver_tree';
import { DataAccessLayer } from '../../types';
interface Metadata {
@@ -40,11 +43,24 @@ interface Metadata {
/**
* A simple mock dataAccessLayer possible that returns a tree with 0 ancestors and 2 direct children. 1 related event is returned. The parameter to `entities` is ignored.
*/
-export function oneAncestorTwoChildren(): { dataAccessLayer: DataAccessLayer; metadata: Metadata } {
+export function oneAncestorTwoChildren(
+ { withRelatedEvents }: { withRelatedEvents: Iterable<[string, string]> | null } = {
+ withRelatedEvents: null,
+ }
+): { dataAccessLayer: DataAccessLayer; metadata: Metadata } {
const metadata: Metadata = {
databaseDocumentID: '_id',
entityIDs: { origin: 'origin', firstChild: 'firstChild', secondChild: 'secondChild' },
};
+ const baseTree = mockTreeWithNoAncestorsAnd2Children({
+ originID: metadata.entityIDs.origin,
+ firstChildID: metadata.entityIDs.firstChild,
+ secondChildID: metadata.entityIDs.secondChild,
+ });
+ const composedTree = withRelatedEvents
+ ? withRelatedEventsOnOrigin(baseTree, withRelatedEvents)
+ : baseTree;
+
return {
metadata,
dataAccessLayer: {
@@ -54,13 +70,17 @@ export function oneAncestorTwoChildren(): { dataAccessLayer: DataAccessLayer; me
relatedEvents(entityID: string): Promise {
return Promise.resolve({
entityID,
- events: [
- mockEndpointEvent({
- entityID,
- name: 'event',
- timestamp: 0,
- }),
- ],
+ events:
+ /* Respond with the mocked related events when the origin's related events are fetched*/ withRelatedEvents &&
+ entityID === metadata.entityIDs.origin
+ ? composedTree.relatedEvents.events
+ : [
+ mockEndpointEvent({
+ entityID,
+ name: 'event',
+ timestamp: 0,
+ }),
+ ],
nextEvent: null,
});
},
@@ -69,13 +89,7 @@ export function oneAncestorTwoChildren(): { dataAccessLayer: DataAccessLayer; me
* Fetch a ResolverTree for a entityID
*/
resolverTree(): Promise {
- return Promise.resolve(
- mockTreeWithNoAncestorsAnd2Children({
- originID: metadata.entityIDs.origin,
- firstChildID: metadata.entityIDs.firstChild,
- secondChildID: metadata.entityIDs.secondChild,
- })
- );
+ return Promise.resolve(composedTree);
},
/**
diff --git a/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/__snapshots__/isometric_taxi_layout.test.ts.snap b/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/__snapshots__/isometric_taxi_layout.test.ts.snap
index 6f26bfe063c05..db8d047c2ce86 100644
--- a/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/__snapshots__/isometric_taxi_layout.test.ts.snap
+++ b/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/__snapshots__/isometric_taxi_layout.test.ts.snap
@@ -182,7 +182,7 @@ Object {
"edgeLineSegments": Array [
Object {
"metadata": Object {
- "uniqueId": "parentToMid",
+ "uniqueId": "parentToMidedge:0:1",
},
"points": Array [
Array [
@@ -197,7 +197,7 @@ Object {
},
Object {
"metadata": Object {
- "uniqueId": "midway",
+ "uniqueId": "midwayedge:0:1",
},
"points": Array [
Array [
@@ -212,7 +212,7 @@ Object {
},
Object {
"metadata": Object {
- "uniqueId": "",
+ "uniqueId": "edge:0:1",
},
"points": Array [
Array [
@@ -227,7 +227,7 @@ Object {
},
Object {
"metadata": Object {
- "uniqueId": "",
+ "uniqueId": "edge:0:2",
},
"points": Array [
Array [
@@ -242,7 +242,7 @@ Object {
},
Object {
"metadata": Object {
- "uniqueId": "",
+ "uniqueId": "edge:0:8",
},
"points": Array [
Array [
@@ -257,7 +257,7 @@ Object {
},
Object {
"metadata": Object {
- "uniqueId": "parentToMid13",
+ "uniqueId": "parentToMidedge:1:3",
},
"points": Array [
Array [
@@ -272,7 +272,7 @@ Object {
},
Object {
"metadata": Object {
- "uniqueId": "midway13",
+ "uniqueId": "midwayedge:1:3",
},
"points": Array [
Array [
@@ -287,7 +287,7 @@ Object {
},
Object {
"metadata": Object {
- "uniqueId": "13",
+ "uniqueId": "edge:1:3",
},
"points": Array [
Array [
@@ -302,7 +302,7 @@ Object {
},
Object {
"metadata": Object {
- "uniqueId": "14",
+ "uniqueId": "edge:1:4",
},
"points": Array [
Array [
@@ -317,7 +317,7 @@ Object {
},
Object {
"metadata": Object {
- "uniqueId": "parentToMid25",
+ "uniqueId": "parentToMidedge:2:5",
},
"points": Array [
Array [
@@ -332,7 +332,7 @@ Object {
},
Object {
"metadata": Object {
- "uniqueId": "midway25",
+ "uniqueId": "midwayedge:2:5",
},
"points": Array [
Array [
@@ -347,7 +347,7 @@ Object {
},
Object {
"metadata": Object {
- "uniqueId": "25",
+ "uniqueId": "edge:2:5",
},
"points": Array [
Array [
@@ -362,7 +362,7 @@ Object {
},
Object {
"metadata": Object {
- "uniqueId": "26",
+ "uniqueId": "edge:2:6",
},
"points": Array [
Array [
@@ -377,7 +377,7 @@ Object {
},
Object {
"metadata": Object {
- "uniqueId": "67",
+ "uniqueId": "edge:6:7",
},
"points": Array [
Array [
@@ -584,7 +584,7 @@ Object {
"edgeLineSegments": Array [
Object {
"metadata": Object {
- "uniqueId": "",
+ "uniqueId": "edge:0:1",
},
"points": Array [
Array [
diff --git a/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/index.ts b/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/index.ts
index 060a014b8730f..f6b893ba25b78 100644
--- a/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/index.ts
+++ b/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/index.ts
@@ -4,10 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { uniquePidForProcess, uniqueParentPidForProcess, orderByTime } from '../process_event';
+import { orderByTime } from '../process_event';
import { IndexedProcessTree } from '../../types';
-import { ResolverEvent } from '../../../../common/endpoint/types';
+import { SafeResolverEvent } from '../../../../common/endpoint/types';
import { levelOrder as baseLevelOrder } from '../../lib/tree_sequencers';
+import * as eventModel from '../../../../common/endpoint/models/event';
/**
* Create a new IndexedProcessTree from an array of ProcessEvents.
@@ -15,24 +16,25 @@ import { levelOrder as baseLevelOrder } from '../../lib/tree_sequencers';
*/
export function factory(
// Array of processes to index as a tree
- processes: ResolverEvent[]
+ processes: SafeResolverEvent[]
): IndexedProcessTree {
- const idToChildren = new Map();
- const idToValue = new Map();
+ const idToChildren = new Map();
+ const idToValue = new Map();
for (const process of processes) {
- const uniqueProcessPid = uniquePidForProcess(process);
- idToValue.set(uniqueProcessPid, process);
+ const entityID: string | undefined = eventModel.entityIDSafeVersion(process);
+ if (entityID !== undefined) {
+ idToValue.set(entityID, process);
- // NB: If the value was null or undefined, use `undefined`
- const uniqueParentPid: string | undefined = uniqueParentPidForProcess(process) ?? undefined;
+ const uniqueParentPid: string | undefined = eventModel.parentEntityIDSafeVersion(process);
- let childrenWithTheSameParent = idToChildren.get(uniqueParentPid);
- if (!childrenWithTheSameParent) {
- childrenWithTheSameParent = [];
- idToChildren.set(uniqueParentPid, childrenWithTheSameParent);
+ let childrenWithTheSameParent = idToChildren.get(uniqueParentPid);
+ if (!childrenWithTheSameParent) {
+ childrenWithTheSameParent = [];
+ idToChildren.set(uniqueParentPid, childrenWithTheSameParent);
+ }
+ childrenWithTheSameParent.push(process);
}
- childrenWithTheSameParent.push(process);
}
// sort the children of each node
@@ -49,7 +51,10 @@ export function factory(
/**
* Returns an array with any children `ProcessEvent`s of the passed in `process`
*/
-export function children(tree: IndexedProcessTree, parentID: string | undefined): ResolverEvent[] {
+export function children(
+ tree: IndexedProcessTree,
+ parentID: string | undefined
+): SafeResolverEvent[] {
const currentProcessSiblings = tree.idToChildren.get(parentID);
return currentProcessSiblings === undefined ? [] : currentProcessSiblings;
}
@@ -57,7 +62,7 @@ export function children(tree: IndexedProcessTree, parentID: string | undefined)
/**
* Get the indexed process event for the ID
*/
-export function processEvent(tree: IndexedProcessTree, entityID: string): ResolverEvent | null {
+export function processEvent(tree: IndexedProcessTree, entityID: string): SafeResolverEvent | null {
return tree.idToProcess.get(entityID) ?? null;
}
@@ -66,9 +71,9 @@ export function processEvent(tree: IndexedProcessTree, entityID: string): Resolv
*/
export function parent(
tree: IndexedProcessTree,
- childProcess: ResolverEvent
-): ResolverEvent | undefined {
- const uniqueParentPid = uniqueParentPidForProcess(childProcess);
+ childProcess: SafeResolverEvent
+): SafeResolverEvent | undefined {
+ const uniqueParentPid = eventModel.parentEntityIDSafeVersion(childProcess);
if (uniqueParentPid === undefined) {
return undefined;
} else {
@@ -91,7 +96,7 @@ export function root(tree: IndexedProcessTree) {
return null;
}
// any node will do
- let current: ResolverEvent = tree.idToProcess.values().next().value;
+ let current: SafeResolverEvent = tree.idToProcess.values().next().value;
// iteratively swap current w/ its parent
while (parent(tree, current) !== undefined) {
@@ -106,8 +111,8 @@ export function root(tree: IndexedProcessTree) {
export function* levelOrder(tree: IndexedProcessTree) {
const rootNode = root(tree);
if (rootNode !== null) {
- yield* baseLevelOrder(rootNode, (parentNode: ResolverEvent): ResolverEvent[] =>
- children(tree, uniquePidForProcess(parentNode))
+ yield* baseLevelOrder(rootNode, (parentNode: SafeResolverEvent): SafeResolverEvent[] =>
+ children(tree, eventModel.entityIDSafeVersion(parentNode))
);
}
}
diff --git a/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/isometric_taxi_layout.ts b/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/isometric_taxi_layout.ts
index 1fc2ea0150aee..f0880fa635a24 100644
--- a/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/isometric_taxi_layout.ts
+++ b/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/isometric_taxi_layout.ts
@@ -14,12 +14,11 @@ import {
Matrix3,
IsometricTaxiLayout,
} from '../../types';
-import * as event from '../../../../common/endpoint/models/event';
-import { ResolverEvent } from '../../../../common/endpoint/types';
+import * as eventModel from '../../../../common/endpoint/models/event';
+import { SafeResolverEvent } from '../../../../common/endpoint/types';
import * as vector2 from '../vector2';
import * as indexedProcessTreeModel from './index';
import { getFriendlyElapsedTime as elapsedTime } from '../../lib/date';
-import { uniquePidForProcess } from '../process_event';
/**
* Graph the process tree
@@ -30,25 +29,29 @@ export function isometricTaxiLayoutFactory(
/**
* Walk the tree in reverse level order, calculating the 'width' of subtrees.
*/
- const widths = widthsOfProcessSubtrees(indexedProcessTree);
+ const widths: Map = widthsOfProcessSubtrees(indexedProcessTree);
/**
* Walk the tree in level order. Using the precalculated widths, calculate the position of nodes.
* Nodes are positioned relative to their parents and preceding siblings.
*/
- const positions = processPositions(indexedProcessTree, widths);
+ const positions: Map = processPositions(indexedProcessTree, widths);
/**
* With the widths and positions precalculated, we calculate edge line segments (arrays of vector2s)
* which connect them in a 'pitchfork' design.
*/
- const edgeLineSegments = processEdgeLineSegments(indexedProcessTree, widths, positions);
+ const edgeLineSegments: EdgeLineSegment[] = processEdgeLineSegments(
+ indexedProcessTree,
+ widths,
+ positions
+ );
/**
* Transform the positions of nodes and edges so they seem like they are on an isometric grid.
*/
const transformedEdgeLineSegments: EdgeLineSegment[] = [];
- const transformedPositions = new Map();
+ const transformedPositions = new Map();
for (const [processEvent, position] of positions) {
transformedPositions.set(
@@ -83,8 +86,8 @@ export function isometricTaxiLayoutFactory(
/**
* Calculate a level (starting at 1) for each node.
*/
-function ariaLevels(indexedProcessTree: IndexedProcessTree): Map {
- const map: Map = new Map();
+function ariaLevels(indexedProcessTree: IndexedProcessTree): Map {
+ const map: Map = new Map();
for (const node of indexedProcessTreeModel.levelOrder(indexedProcessTree)) {
const parentNode = indexedProcessTreeModel.parent(indexedProcessTree, node);
if (parentNode === undefined) {
@@ -143,20 +146,20 @@ function ariaLevels(indexedProcessTree: IndexedProcessTree): Map();
+ const widths = new Map();
if (indexedProcessTreeModel.size(indexedProcessTree) === 0) {
return widths;
}
- const processesInReverseLevelOrder: ResolverEvent[] = [
+ const processesInReverseLevelOrder: SafeResolverEvent[] = [
...indexedProcessTreeModel.levelOrder(indexedProcessTree),
].reverse();
for (const process of processesInReverseLevelOrder) {
const children = indexedProcessTreeModel.children(
indexedProcessTree,
- uniquePidForProcess(process)
+ eventModel.entityIDSafeVersion(process)
);
const sumOfWidthOfChildren = function sumOfWidthOfChildren() {
@@ -167,7 +170,7 @@ function widthsOfProcessSubtrees(indexedProcessTree: IndexedProcessTree): Proces
* Therefore a parent can always find a width for its children, since all of its children
* will have been handled already.
*/
- return currentValue + widths.get(child)!;
+ return currentValue + (widths.get(child) ?? 0);
}, 0);
};
@@ -178,6 +181,9 @@ function widthsOfProcessSubtrees(indexedProcessTree: IndexedProcessTree): Proces
return widths;
}
+/**
+ * Layout the graph. Note: if any process events are missing the `entity_id`, this will throw an Error.
+ */
function processEdgeLineSegments(
indexedProcessTree: IndexedProcessTree,
widths: ProcessWidths,
@@ -196,9 +202,13 @@ function processEdgeLineSegments(
const { process, parent, parentWidth } = metadata;
const position = positions.get(process);
const parentPosition = positions.get(parent);
- const parentId = event.entityId(parent);
- const processEntityId = event.entityId(process);
- const edgeLineId = parentId ? parentId + processEntityId : parentId;
+ const parentID = eventModel.entityIDSafeVersion(parent);
+ const processEntityID = eventModel.entityIDSafeVersion(process);
+
+ if (processEntityID === undefined) {
+ throw new Error('tried to graph a Resolver that had a process with no `process.entity_id`');
+ }
+ const edgeLineID = `edge:${parentID ?? 'undefined'}:${processEntityID}`;
if (position === undefined || parentPosition === undefined) {
/**
@@ -207,12 +217,12 @@ function processEdgeLineSegments(
throw new Error();
}
- const parentTime = event.eventTimestamp(parent);
- const processTime = event.eventTimestamp(process);
+ const parentTime = eventModel.timestampSafeVersion(parent);
+ const processTime = eventModel.timestampSafeVersion(process);
if (parentTime && processTime) {
edgeLineMetadata.elapsedTime = elapsedTime(parentTime, processTime) ?? undefined;
}
- edgeLineMetadata.uniqueId = edgeLineId;
+ edgeLineMetadata.uniqueId = edgeLineID;
/**
* The point halfway between the parent and child on the y axis, we sometimes have a hard angle here in the edge line
@@ -236,7 +246,7 @@ function processEdgeLineSegments(
const siblings = indexedProcessTreeModel.children(
indexedProcessTree,
- uniquePidForProcess(parent)
+ eventModel.entityIDSafeVersion(parent)
);
const isFirstChild = process === siblings[0];
@@ -260,7 +270,7 @@ function processEdgeLineSegments(
const lineFromParentToMidwayLine: EdgeLineSegment = {
points: [parentPosition, [parentPosition[0], midwayY]],
- metadata: { uniqueId: `parentToMid${edgeLineId}` },
+ metadata: { uniqueId: `parentToMid${edgeLineID}` },
};
const widthOfMidline = parentWidth - firstChildWidth / 2 - lastChildWidth / 2;
@@ -281,7 +291,7 @@ function processEdgeLineSegments(
midwayY,
],
],
- metadata: { uniqueId: `midway${edgeLineId}` },
+ metadata: { uniqueId: `midway${edgeLineID}` },
};
edgeLineSegments.push(
@@ -303,13 +313,13 @@ function processPositions(
indexedProcessTree: IndexedProcessTree,
widths: ProcessWidths
): ProcessPositions {
- const positions = new Map();
+ const positions = new Map();
/**
* This algorithm iterates the tree in level order. It keeps counters that are reset for each parent.
* By keeping track of the last parent node, we can know when we are dealing with a new set of siblings and
* reset the counters.
*/
- let lastProcessedParentNode: ResolverEvent | undefined;
+ let lastProcessedParentNode: SafeResolverEvent | undefined;
/**
* Nodes are positioned relative to their siblings. We walk this in level order, so we handle
* children left -> right.
@@ -431,7 +441,10 @@ function* levelOrderWithWidths(
parentWidth,
};
- const siblings = indexedProcessTreeModel.children(tree, uniquePidForProcess(parent));
+ const siblings = indexedProcessTreeModel.children(
+ tree,
+ eventModel.entityIDSafeVersion(parent)
+ );
if (siblings.length === 1) {
metadata.isOnlyChild = true;
metadata.lastChildWidth = width;
@@ -488,7 +501,10 @@ const distanceBetweenNodesInUnits = 2;
*/
const distanceBetweenNodes = distanceBetweenNodesInUnits * unit;
-export function nodePosition(model: IsometricTaxiLayout, node: ResolverEvent): Vector2 | undefined {
+export function nodePosition(
+ model: IsometricTaxiLayout,
+ node: SafeResolverEvent
+): Vector2 | undefined {
return model.processNodePositions.get(node);
}
diff --git a/x-pack/plugins/security_solution/public/resolver/models/process_event.test.ts b/x-pack/plugins/security_solution/public/resolver/models/process_event.test.ts
index 4b1d555d0a7c3..4d48b34fb2841 100644
--- a/x-pack/plugins/security_solution/public/resolver/models/process_event.test.ts
+++ b/x-pack/plugins/security_solution/public/resolver/models/process_event.test.ts
@@ -6,7 +6,11 @@
import { eventType, orderByTime, userInfoForProcess } from './process_event';
import { mockProcessEvent } from './process_event_test_helpers';
-import { LegacyEndpointEvent, ResolverEvent } from '../../../common/endpoint/types';
+import {
+ LegacyEndpointEvent,
+ ResolverEvent,
+ SafeResolverEvent,
+} from '../../../common/endpoint/types';
describe('process event', () => {
describe('eventType', () => {
@@ -42,7 +46,7 @@ describe('process event', () => {
});
describe('orderByTime', () => {
let mock: (time: number, eventID: string) => ResolverEvent;
- let events: ResolverEvent[];
+ let events: SafeResolverEvent[];
beforeEach(() => {
mock = (time, eventID) => {
return {
@@ -56,14 +60,14 @@ describe('process event', () => {
// each event has a unique id, a through h
// order is arbitrary
events = [
- mock(-1, 'a'),
- mock(0, 'c'),
- mock(1, 'e'),
- mock(NaN, 'g'),
- mock(-1, 'b'),
- mock(0, 'd'),
- mock(1, 'f'),
- mock(NaN, 'h'),
+ mock(-1, 'a') as SafeResolverEvent,
+ mock(0, 'c') as SafeResolverEvent,
+ mock(1, 'e') as SafeResolverEvent,
+ mock(NaN, 'g') as SafeResolverEvent,
+ mock(-1, 'b') as SafeResolverEvent,
+ mock(0, 'd') as SafeResolverEvent,
+ mock(1, 'f') as SafeResolverEvent,
+ mock(NaN, 'h') as SafeResolverEvent,
];
});
it('sorts events as expected', () => {
diff --git a/x-pack/plugins/security_solution/public/resolver/models/process_event.ts b/x-pack/plugins/security_solution/public/resolver/models/process_event.ts
index 1a5c67f6a6f2f..ea588731a55c8 100644
--- a/x-pack/plugins/security_solution/public/resolver/models/process_event.ts
+++ b/x-pack/plugins/security_solution/public/resolver/models/process_event.ts
@@ -5,7 +5,7 @@
*/
import * as event from '../../../common/endpoint/models/event';
-import { ResolverEvent } from '../../../common/endpoint/types';
+import { ResolverEvent, SafeResolverEvent } from '../../../common/endpoint/types';
import { ResolverProcessType } from '../types';
/**
@@ -32,8 +32,8 @@ export function isTerminatedProcess(passedEvent: ResolverEvent) {
* ms since Unix epoc, based on timestamp.
* may return NaN if the timestamp wasn't present or was invalid.
*/
-export function datetime(passedEvent: ResolverEvent): number | null {
- const timestamp = event.eventTimestamp(passedEvent);
+export function datetime(passedEvent: SafeResolverEvent): number | null {
+ const timestamp = event.timestampSafeVersion(passedEvent);
const time = timestamp === undefined ? 0 : new Date(timestamp).getTime();
@@ -178,13 +178,15 @@ export function argsForProcess(passedEvent: ResolverEvent): string | undefined {
/**
* used to sort events
*/
-export function orderByTime(first: ResolverEvent, second: ResolverEvent): number {
+export function orderByTime(first: SafeResolverEvent, second: SafeResolverEvent): number {
const firstDatetime: number | null = datetime(first);
const secondDatetime: number | null = datetime(second);
if (firstDatetime === secondDatetime) {
// break ties using an arbitrary (stable) comparison of `eventId` (which should be unique)
- return String(event.eventId(first)).localeCompare(String(event.eventId(second)));
+ return String(event.eventIDSafeVersion(first)).localeCompare(
+ String(event.eventIDSafeVersion(second))
+ );
} else if (firstDatetime === null || secondDatetime === null) {
// sort `null`'s as higher than numbers
return (firstDatetime === null ? 1 : 0) - (secondDatetime === null ? 1 : 0);
diff --git a/x-pack/plugins/security_solution/public/resolver/store/actions.ts b/x-pack/plugins/security_solution/public/resolver/store/actions.ts
index 418eb0d837276..29c03215e9ff4 100644
--- a/x-pack/plugins/security_solution/public/resolver/store/actions.ts
+++ b/x-pack/plugins/security_solution/public/resolver/store/actions.ts
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { CameraAction } from './camera';
-import { ResolverEvent } from '../../../common/endpoint/types';
+import { ResolverEvent, SafeResolverEvent } from '../../../common/endpoint/types';
import { DataAction } from './data/action';
/**
@@ -96,7 +96,7 @@ interface UserSelectedResolverNode {
interface UserSelectedRelatedEventCategory {
readonly type: 'userSelectedRelatedEventCategory';
readonly payload: {
- subject: ResolverEvent;
+ subject: SafeResolverEvent;
category?: string;
};
}
diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts
index 272d0aae7eef4..569a24bb8537e 100644
--- a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts
+++ b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts
@@ -28,10 +28,11 @@ import {
ResolverTree,
ResolverNodeStats,
ResolverRelatedEvents,
+ SafeResolverEvent,
} from '../../../../common/endpoint/types';
import * as resolverTreeModel from '../../models/resolver_tree';
import * as isometricTaxiLayoutModel from '../../models/indexed_process_tree/isometric_taxi_layout';
-import { allEventCategories } from '../../../../common/endpoint/models/event';
+import * as eventModel from '../../../../common/endpoint/models/event';
import * as vector2 from '../../models/vector2';
/**
@@ -145,7 +146,7 @@ export const tree = createSelector(graphableProcesses, function indexedTree(
graphableProcesses
/* eslint-enable no-shadow */
) {
- return indexedProcessTreeModel.factory(graphableProcesses);
+ return indexedProcessTreeModel.factory(graphableProcesses as SafeResolverEvent[]);
});
/**
@@ -194,7 +195,9 @@ export const relatedEventsByCategory: (
}
return relatedById.events.reduce(
(eventsByCategory: ResolverEvent[], candidate: ResolverEvent) => {
- if ([candidate && allEventCategories(candidate)].flat().includes(ecsCategory)) {
+ if (
+ [candidate && eventModel.allEventCategories(candidate)].flat().includes(ecsCategory)
+ ) {
eventsByCategory.push(candidate);
}
return eventsByCategory;
@@ -280,7 +283,7 @@ export const relatedEventInfoByEntityId: (
return [];
}
return eventsResponseForThisEntry.events.filter((resolverEvent) => {
- for (const category of [allEventCategories(resolverEvent)].flat()) {
+ for (const category of [eventModel.allEventCategories(resolverEvent)].flat()) {
if (category === eventCategory) {
return true;
}
@@ -404,7 +407,7 @@ export const processEventForID: (
) => (nodeID: string) => ResolverEvent | null = createSelector(
tree,
(indexedProcessTree) => (nodeID: string) =>
- indexedProcessTreeModel.processEvent(indexedProcessTree, nodeID)
+ indexedProcessTreeModel.processEvent(indexedProcessTree, nodeID) as ResolverEvent
);
/**
@@ -415,7 +418,7 @@ export const ariaLevel: (state: DataState) => (nodeID: string) => number | null
processEventForID,
({ ariaLevels }, processEventGetter) => (nodeID: string) => {
const node = processEventGetter(nodeID);
- return node ? ariaLevels.get(node) ?? null : null;
+ return node ? ariaLevels.get(node as SafeResolverEvent) ?? null : null;
}
);
@@ -468,10 +471,10 @@ export const ariaFlowtoCandidate: (
for (const child of children) {
if (previousChild !== null) {
// Set the `child` as the following sibling of `previousChild`.
- memo.set(uniquePidForProcess(previousChild), uniquePidForProcess(child));
+ memo.set(uniquePidForProcess(previousChild), uniquePidForProcess(child as ResolverEvent));
}
// Set the child as the previous child.
- previousChild = child;
+ previousChild = child as ResolverEvent;
}
if (previousChild) {
@@ -553,7 +556,7 @@ export const nodesAndEdgelines: (
maxX,
maxY,
});
- const visibleProcessNodePositions = new Map(
+ const visibleProcessNodePositions = new Map(
entities
.filter((entity): entity is IndexedProcessNode => entity.type === 'processNode')
.map((node) => [node.entity, node.position])
diff --git a/x-pack/plugins/security_solution/public/resolver/store/methods.ts b/x-pack/plugins/security_solution/public/resolver/store/methods.ts
index ad06ddf36161a..8dd15b1a44d0c 100644
--- a/x-pack/plugins/security_solution/public/resolver/store/methods.ts
+++ b/x-pack/plugins/security_solution/public/resolver/store/methods.ts
@@ -7,7 +7,7 @@
import { animatePanning } from './camera/methods';
import { layout } from './selectors';
import { ResolverState } from '../types';
-import { ResolverEvent } from '../../../common/endpoint/types';
+import { ResolverEvent, SafeResolverEvent } from '../../../common/endpoint/types';
const animationDuration = 1000;
@@ -20,7 +20,7 @@ export function animateProcessIntoView(
process: ResolverEvent
): ResolverState {
const { processNodePositions } = layout(state);
- const position = processNodePositions.get(process);
+ const position = processNodePositions.get(process as SafeResolverEvent);
if (position) {
return {
...state,
diff --git a/x-pack/plugins/security_solution/public/resolver/store/mocks/related_event.ts b/x-pack/plugins/security_solution/public/resolver/store/mocks/related_event.ts
new file mode 100644
index 0000000000000..1e0c460a3a711
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/resolver/store/mocks/related_event.ts
@@ -0,0 +1,36 @@
+/*
+ * 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 { EndpointEvent } from '../../../../common/endpoint/types';
+
+/**
+ * Simple mock related event.
+ */
+export function mockRelatedEvent({
+ entityID,
+ timestamp,
+ category,
+ type,
+ id,
+}: {
+ entityID: string;
+ timestamp: number;
+ category: string;
+ type: string;
+ id?: string;
+}): EndpointEvent {
+ return {
+ '@timestamp': timestamp,
+ event: {
+ kind: 'event',
+ type,
+ category,
+ id: id ?? 'xyz',
+ },
+ process: {
+ entity_id: entityID,
+ },
+ } as EndpointEvent;
+}
diff --git a/x-pack/plugins/security_solution/public/resolver/store/mocks/resolver_tree.ts b/x-pack/plugins/security_solution/public/resolver/store/mocks/resolver_tree.ts
index 6a8ab61ccf9b6..21d0309501aa8 100644
--- a/x-pack/plugins/security_solution/public/resolver/store/mocks/resolver_tree.ts
+++ b/x-pack/plugins/security_solution/public/resolver/store/mocks/resolver_tree.ts
@@ -5,6 +5,7 @@
*/
import { mockEndpointEvent } from './endpoint_event';
+import { mockRelatedEvent } from './related_event';
import { ResolverTree, ResolverEvent } from '../../../../common/endpoint/types';
export function mockTreeWith2AncestorsAndNoChildren({
@@ -109,6 +110,58 @@ export function mockTreeWithAllProcessesTerminated({
} as unknown) as ResolverTree;
}
+/**
+ * A valid category for a related event. E.g. "registry", "network", "file"
+ */
+type RelatedEventCategory = string;
+/**
+ * A valid type for a related event. E.g. "start", "end", "access"
+ */
+type RelatedEventType = string;
+
+/**
+ * Add/replace related event info (on origin node) for any mock ResolverTree
+ *
+ * @param treeToAddRelatedEventsTo the ResolverTree to modify
+ * @param relatedEventsToAddByCategoryAndType Iterable of `[category, type]` pairs describing related events. e.g. [['dns','info'],['registry','access']]
+ */
+export function withRelatedEventsOnOrigin(
+ treeToAddRelatedEventsTo: ResolverTree,
+ relatedEventsToAddByCategoryAndType: Iterable<[RelatedEventCategory, RelatedEventType]>
+): ResolverTree {
+ const events = [];
+ const byCategory: Record = {};
+ const stats = {
+ totalAlerts: 0,
+ events: {
+ total: 0,
+ byCategory,
+ },
+ };
+ for (const [category, type] of relatedEventsToAddByCategoryAndType) {
+ events.push(
+ mockRelatedEvent({
+ entityID: treeToAddRelatedEventsTo.entityID,
+ timestamp: 1,
+ category,
+ type,
+ })
+ );
+ stats.events.total++;
+ stats.events.byCategory[category] = stats.events.byCategory[category]
+ ? stats.events.byCategory[category] + 1
+ : 1;
+ }
+ return {
+ ...treeToAddRelatedEventsTo,
+ stats,
+ relatedEvents: {
+ events,
+ nextEvent: null,
+ },
+ };
+}
+
export function mockTreeWithNoAncestorsAnd2Children({
originID,
firstChildID,
diff --git a/x-pack/plugins/security_solution/public/resolver/store/selectors.test.ts b/x-pack/plugins/security_solution/public/resolver/store/selectors.test.ts
index df365a078b27f..dfbc6bd290686 100644
--- a/x-pack/plugins/security_solution/public/resolver/store/selectors.test.ts
+++ b/x-pack/plugins/security_solution/public/resolver/store/selectors.test.ts
@@ -13,6 +13,7 @@ import {
mockTreeWith2AncestorsAndNoChildren,
mockTreeWithNoAncestorsAnd2Children,
} from './mocks/resolver_tree';
+import { SafeResolverEvent } from '../../../common/endpoint/types';
describe('resolver selectors', () => {
const actions: ResolverAction[] = [];
@@ -114,7 +115,9 @@ describe('resolver selectors', () => {
// find the position of the second child
const secondChild = selectors.processEventForID(state())(secondChildID);
- const positionOfSecondChild = layout.processNodePositions.get(secondChild!)!;
+ const positionOfSecondChild = layout.processNodePositions.get(
+ secondChild as SafeResolverEvent
+ )!;
// the child is indexed by an AABB that extends -720/2 to the left
const leftSideOfSecondChildAABB = positionOfSecondChild[0] - 720 / 2;
@@ -130,19 +133,25 @@ describe('resolver selectors', () => {
it('the origin should be in view', () => {
const origin = selectors.processEventForID(state())(originID)!;
expect(
- selectors.visibleNodesAndEdgeLines(state())(0).processNodePositions.has(origin)
+ selectors
+ .visibleNodesAndEdgeLines(state())(0)
+ .processNodePositions.has(origin as SafeResolverEvent)
).toBe(true);
});
it('the first child should be in view', () => {
const firstChild = selectors.processEventForID(state())(firstChildID)!;
expect(
- selectors.visibleNodesAndEdgeLines(state())(0).processNodePositions.has(firstChild)
+ selectors
+ .visibleNodesAndEdgeLines(state())(0)
+ .processNodePositions.has(firstChild as SafeResolverEvent)
).toBe(true);
});
it('the second child should not be in view', () => {
const secondChild = selectors.processEventForID(state())(secondChildID)!;
expect(
- selectors.visibleNodesAndEdgeLines(state())(0).processNodePositions.has(secondChild)
+ selectors
+ .visibleNodesAndEdgeLines(state())(0)
+ .processNodePositions.has(secondChild as SafeResolverEvent)
).toBe(false);
});
it('should return nothing as the flowto for the first child', () => {
diff --git a/x-pack/plugins/security_solution/public/resolver/store/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/selectors.ts
index 87ef8d5d095ef..70a461909a99b 100644
--- a/x-pack/plugins/security_solution/public/resolver/store/selectors.ts
+++ b/x-pack/plugins/security_solution/public/resolver/store/selectors.ts
@@ -9,8 +9,8 @@ import * as cameraSelectors from './camera/selectors';
import * as dataSelectors from './data/selectors';
import * as uiSelectors from './ui/selectors';
import { ResolverState, IsometricTaxiLayout } from '../types';
-import { uniquePidForProcess } from '../models/process_event';
import { ResolverEvent, ResolverNodeStats } from '../../../common/endpoint/types';
+import { entityIDSafeVersion } from '../../../common/endpoint/models/event';
/**
* A matrix that when applied to a Vector2 will convert it from world coordinates to screen coordinates.
@@ -271,9 +271,14 @@ export const ariaFlowtoNodeID: (
const { processNodePositions } = visibleNodesAndEdgeLinesAtTime(time);
// get a `Set` containing their node IDs
- const nodesVisibleAtTime: Set = new Set(
- [...processNodePositions.keys()].map(uniquePidForProcess)
- );
+ const nodesVisibleAtTime: Set = new Set();
+ // NB: in practice, any event that has been graphed is guaranteed to have an entity_id
+ for (const visibleEvent of processNodePositions.keys()) {
+ const nodeID = entityIDSafeVersion(visibleEvent);
+ if (nodeID !== undefined) {
+ nodesVisibleAtTime.add(nodeID);
+ }
+ }
// return the ID of `nodeID`'s following sibling, if it is visible
return (nodeID: string): string | null => {
diff --git a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx
index 2a2354921a3d4..ed30643ed871e 100644
--- a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx
+++ b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx
@@ -220,6 +220,28 @@ export class Simulator {
);
}
+ /**
+ * Dump all contents of the outer ReactWrapper (to be `console.log`ged as appropriate)
+ * This will include both DOM (div, span, etc.) and React/JSX (MyComponent, MyGrid, etc.)
+ */
+ public debugWrapper() {
+ return this.wrapper.debug();
+ }
+
+ /**
+ * Return an Enzyme ReactWrapper that includes the Related Events host button for a given process node
+ *
+ * @param entityID The entity ID of the proocess node to select in
+ */
+ public processNodeRelatedEventButton(entityID: string): ReactWrapper {
+ return this.processNodeElements({ entityID }).findWhere(
+ (wrapper) =>
+ // Filter out React components
+ typeof wrapper.type() === 'string' &&
+ wrapper.prop('data-test-subj') === 'resolver:submenu:button'
+ );
+ }
+
/**
* Return the selected node query string values.
*/
diff --git a/x-pack/plugins/security_solution/public/resolver/types.ts b/x-pack/plugins/security_solution/public/resolver/types.ts
index c2871fdceb20a..30634e722050f 100644
--- a/x-pack/plugins/security_solution/public/resolver/types.ts
+++ b/x-pack/plugins/security_solution/public/resolver/types.ts
@@ -11,10 +11,10 @@ import { Middleware, Dispatch } from 'redux';
import { BBox } from 'rbush';
import { ResolverAction } from './store/actions';
import {
- ResolverEvent,
ResolverRelatedEvents,
ResolverTree,
ResolverEntityIndex,
+ SafeResolverEvent,
} from '../../common/endpoint/types';
/**
@@ -155,7 +155,7 @@ export interface IndexedEdgeLineSegment extends BBox {
*/
export interface IndexedProcessNode extends BBox {
type: 'processNode';
- entity: ResolverEvent;
+ entity: SafeResolverEvent;
position: Vector2;
}
@@ -280,21 +280,21 @@ export interface IndexedProcessTree {
/**
* Map of ID to a process's ordered children
*/
- idToChildren: Map;
+ idToChildren: Map;
/**
* Map of ID to process
*/
- idToProcess: Map;
+ idToProcess: Map;
}
/**
* A map of `ProcessEvents` (representing process nodes) to the 'width' of their subtrees as calculated by `widthsOfProcessSubtrees`
*/
-export type ProcessWidths = Map;
+export type ProcessWidths = Map;
/**
* Map of ProcessEvents (representing process nodes) to their positions. Calculated by `processPositions`
*/
-export type ProcessPositions = Map;
+export type ProcessPositions = Map;
export type DurationTypes =
| 'millisecond'
@@ -346,11 +346,11 @@ export interface EdgeLineSegment {
* Used to provide pre-calculated info from `widthsOfProcessSubtrees`. These 'width' values are used in the layout of the graph.
*/
export type ProcessWithWidthMetadata = {
- process: ResolverEvent;
+ process: SafeResolverEvent;
width: number;
} & (
| {
- parent: ResolverEvent;
+ parent: SafeResolverEvent;
parentWidth: number;
isOnlyChild: boolean;
firstChildWidth: number;
@@ -433,7 +433,7 @@ export interface IsometricTaxiLayout {
/**
* A map of events to position. Each event represents its own node.
*/
- processNodePositions: Map;
+ processNodePositions: Map;
/**
* A map of edge-line segments, which graphically connect nodes.
*/
@@ -442,7 +442,7 @@ export interface IsometricTaxiLayout {
/**
* defines the aria levels for nodes.
*/
- ariaLevels: Map;
+ ariaLevels: Map;
}
/**
diff --git a/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx
index f339d128944cc..c819491dd28f0 100644
--- a/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx
+++ b/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx
@@ -9,14 +9,14 @@ import { Simulator } from '../test_utilities/simulator';
// Extend jest with a custom matcher
import '../test_utilities/extend_jest';
-describe('Resolver, when analyzing a tree that has 1 ancestor and 2 children', () => {
- let simulator: Simulator;
- let databaseDocumentID: string;
- let entityIDs: { origin: string; firstChild: string; secondChild: string };
+let simulator: Simulator;
+let databaseDocumentID: string;
+let entityIDs: { origin: string; firstChild: string; secondChild: string };
- // the resolver component instance ID, used by the react code to distinguish piece of global state from those used by other resolver instances
- const resolverComponentInstanceID = 'resolverComponentInstanceID';
+// the resolver component instance ID, used by the react code to distinguish piece of global state from those used by other resolver instances
+const resolverComponentInstanceID = 'resolverComponentInstanceID';
+describe('Resolver, when analyzing a tree that has 1 ancestor and 2 children', () => {
beforeEach(async () => {
// create a mock data access layer
const { metadata: dataAccessLayerMetadata, dataAccessLayer } = oneAncestorTwoChildren();
@@ -79,6 +79,7 @@ describe('Resolver, when analyzing a tree that has 1 ancestor and 2 children', (
simulator
.processNodeElements({ entityID: entityIDs.secondChild })
.find('button')
+ .first()
.simulate('click');
});
it('should render the second child node as selected, and the first child not as not selected, and the query string should indicate that the second child is selected', async () => {
@@ -107,3 +108,52 @@ describe('Resolver, when analyzing a tree that has 1 ancestor and 2 children', (
});
});
});
+
+describe('Resolver, when analyzing a tree that has some related events', () => {
+ beforeEach(async () => {
+ // create a mock data access layer with related events
+ const { metadata: dataAccessLayerMetadata, dataAccessLayer } = oneAncestorTwoChildren({
+ withRelatedEvents: [
+ ['registry', 'access'],
+ ['registry', 'access'],
+ ],
+ });
+
+ // save a reference to the entity IDs exposed by the mock data layer
+ entityIDs = dataAccessLayerMetadata.entityIDs;
+
+ // save a reference to the `_id` supported by the mock data layer
+ databaseDocumentID = dataAccessLayerMetadata.databaseDocumentID;
+
+ // create a resolver simulator, using the data access layer and an arbitrary component instance ID
+ simulator = new Simulator({ databaseDocumentID, dataAccessLayer, resolverComponentInstanceID });
+ });
+
+ describe('when it has loaded', () => {
+ beforeEach(async () => {
+ await expect(
+ simulator.mapStateTransitions(() => ({
+ graphElements: simulator.graphElement().length,
+ graphLoadingElements: simulator.graphLoadingElement().length,
+ graphErrorElements: simulator.graphErrorElement().length,
+ originNode: simulator.processNodeElements({ entityID: entityIDs.origin }).length,
+ }))
+ ).toYieldEqualTo({
+ graphElements: 1,
+ graphLoadingElements: 0,
+ graphErrorElements: 0,
+ originNode: 1,
+ });
+ });
+
+ it('should render a related events button', async () => {
+ await expect(
+ simulator.mapStateTransitions(() => ({
+ relatedEventButtons: simulator.processNodeRelatedEventButton(entityIDs.origin).length,
+ }))
+ ).toYieldEqualTo({
+ relatedEventButtons: 1,
+ });
+ });
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/resolver/view/map.tsx b/x-pack/plugins/security_solution/public/resolver/view/map.tsx
index a965f06c04926..bbff2388af8b7 100644
--- a/x-pack/plugins/security_solution/public/resolver/view/map.tsx
+++ b/x-pack/plugins/security_solution/public/resolver/view/map.tsx
@@ -20,7 +20,7 @@ import { SymbolDefinitions, useResolverTheme } from './assets';
import { useStateSyncingActions } from './use_state_syncing_actions';
import { useResolverQueryParams } from './use_resolver_query_params';
import { StyledMapContainer, StyledPanel, GraphContainer } from './styles';
-import { entityId } from '../../../common/endpoint/models/event';
+import { entityIDSafeVersion } from '../../../common/endpoint/models/event';
import { SideEffectContext } from './side_effect_context';
/**
@@ -107,7 +107,7 @@ export const ResolverMap = React.memo(function ({
/>
))}
{[...processNodePositions].map(([processEvent, position]) => {
- const processEntityId = entityId(processEvent);
+ const processEntityId = entityIDSafeVersion(processEvent);
return (
unknown;
-}) {
- interface ProcessTableView {
- name: string;
- timestamp?: Date;
- event: ResolverEvent;
- }
-
- const dispatch = useResolverDispatch();
- const { timestamp } = useContext(SideEffectContext);
- const isProcessTerminated = useSelector(selectors.isProcessTerminated);
- const handleBringIntoViewClick = useCallback(
- (processTableViewItem) => {
- dispatch({
- type: 'userBroughtProcessIntoView',
- payload: {
- time: timestamp(),
- process: processTableViewItem.event,
- },
- });
- pushToQueryParams({ crumbId: event.entityId(processTableViewItem.event), crumbEvent: '' });
- },
- [dispatch, timestamp, pushToQueryParams]
- );
-
- const columns = useMemo>>(
- () => [
- {
- field: 'name',
- name: i18n.translate(
- 'xpack.securitySolution.endpoint.resolver.panel.table.row.processNameTitle',
- {
- defaultMessage: 'Process Name',
- }
- ),
- sortable: true,
- truncateText: true,
- render(name: string, item: ProcessTableView) {
- const entityId = event.entityId(item.event);
- const isTerminated = isProcessTerminated(entityId);
- return name === '' ? (
-
- {i18n.translate(
- 'xpack.securitySolution.endpoint.resolver.panel.table.row.valueMissingDescription',
- {
- defaultMessage: 'Value is missing',
- }
- )}
-
- ) : (
- {
- handleBringIntoViewClick(item);
- pushToQueryParams({ crumbId: event.entityId(item.event), crumbEvent: '' });
- }}
- >
-
- {name}
-
- );
- },
- },
- {
- field: 'timestamp',
- name: i18n.translate(
- 'xpack.securitySolution.endpoint.resolver.panel.table.row.timestampTitle',
- {
- defaultMessage: 'Timestamp',
- }
- ),
- dataType: 'date',
- sortable: true,
- render(eventDate?: Date) {
- return eventDate ? (
- formatter.format(eventDate)
- ) : (
-
- {i18n.translate(
- 'xpack.securitySolution.endpoint.resolver.panel.table.row.timestampInvalidLabel',
- {
- defaultMessage: 'invalid',
- }
- )}
-
- );
- },
- },
- ],
- [pushToQueryParams, handleBringIntoViewClick, isProcessTerminated]
- );
-
- const { processNodePositions } = useSelector(selectors.layout);
- const processTableView: ProcessTableView[] = useMemo(
- () =>
- [...processNodePositions.keys()].map((processEvent) => {
- let dateTime;
- const eventTime = event.eventTimestamp(processEvent);
- const name = event.eventName(processEvent);
- if (eventTime) {
- const date = new Date(eventTime);
- if (isFinite(date.getTime())) {
- dateTime = date;
- }
- }
- return {
- name,
- timestamp: dateTime,
- event: processEvent,
- };
- }),
- [processNodePositions]
- );
- const numberOfProcesses = processTableView.length;
-
- const crumbs = useMemo(() => {
- return [
- {
- text: i18n.translate(
- 'xpack.securitySolution.endpoint.resolver.panel.processListWithCounts.events',
- {
- defaultMessage: 'All Process Events',
- }
- ),
- onClick: () => {},
- },
- ];
- }, []);
-
- const children = useSelector(selectors.hasMoreChildren);
- const ancestors = useSelector(selectors.hasMoreAncestors);
- const showWarning = children === true || ancestors === true;
- return (
- <>
-
- {showWarning && }
-
-
- data-test-subj="resolver:panel:process-list"
- items={processTableView}
- columns={columns}
- sorting
- />
- >
- );
-});
-ProcessListWithCounts.displayName = 'ProcessListWithCounts';
+/*
+ * 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 React, { memo, useContext, useCallback, useMemo } from 'react';
+import {
+ EuiBasicTableColumn,
+ EuiBadge,
+ EuiButtonEmpty,
+ EuiSpacer,
+ EuiInMemoryTable,
+} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { useSelector } from 'react-redux';
+import styled from 'styled-components';
+import * as event from '../../../../common/endpoint/models/event';
+import * as selectors from '../../store/selectors';
+import { CrumbInfo, formatter, StyledBreadcrumbs } from './panel_content_utilities';
+import { useResolverDispatch } from '../use_resolver_dispatch';
+import { SideEffectContext } from '../side_effect_context';
+import { CubeForProcess } from './process_cube_icon';
+import { SafeResolverEvent } from '../../../../common/endpoint/types';
+import { LimitWarning } from '../limit_warnings';
+
+const StyledLimitWarning = styled(LimitWarning)`
+ flex-flow: row wrap;
+ display: block;
+ align-items: baseline;
+ margin-top: 1em;
+
+ & .euiCallOutHeader {
+ display: inline;
+ margin-right: 0.25em;
+ }
+
+ & .euiText {
+ display: inline;
+ }
+
+ & .euiText p {
+ display: inline;
+ }
+`;
+
+/**
+ * The "default" view for the panel: A list of all the processes currently in the graph.
+ *
+ * @param {function} pushToQueryparams A function to update the hash value in the URL to control panel state
+ */
+export const ProcessListWithCounts = memo(function ProcessListWithCounts({
+ pushToQueryParams,
+}: {
+ pushToQueryParams: (queryStringKeyValuePair: CrumbInfo) => unknown;
+}) {
+ interface ProcessTableView {
+ name?: string;
+ timestamp?: Date;
+ event: SafeResolverEvent;
+ }
+
+ const dispatch = useResolverDispatch();
+ const { timestamp } = useContext(SideEffectContext);
+ const isProcessTerminated = useSelector(selectors.isProcessTerminated);
+ const handleBringIntoViewClick = useCallback(
+ (processTableViewItem) => {
+ dispatch({
+ type: 'userBroughtProcessIntoView',
+ payload: {
+ time: timestamp(),
+ process: processTableViewItem.event,
+ },
+ });
+ pushToQueryParams({ crumbId: event.entityId(processTableViewItem.event), crumbEvent: '' });
+ },
+ [dispatch, timestamp, pushToQueryParams]
+ );
+
+ const columns = useMemo>>(
+ () => [
+ {
+ field: 'name',
+ name: i18n.translate(
+ 'xpack.securitySolution.endpoint.resolver.panel.table.row.processNameTitle',
+ {
+ defaultMessage: 'Process Name',
+ }
+ ),
+ sortable: true,
+ truncateText: true,
+ render(name: string, item: ProcessTableView) {
+ const entityID = event.entityIDSafeVersion(item.event);
+ const isTerminated = entityID === undefined ? false : isProcessTerminated(entityID);
+ return name === '' ? (
+
+ {i18n.translate(
+ 'xpack.securitySolution.endpoint.resolver.panel.table.row.valueMissingDescription',
+ {
+ defaultMessage: 'Value is missing',
+ }
+ )}
+
+ ) : (
+ {
+ handleBringIntoViewClick(item);
+ pushToQueryParams({
+ // Take the user back to the list of nodes if this node has no ID
+ crumbId: event.entityIDSafeVersion(item.event) ?? '',
+ crumbEvent: '',
+ });
+ }}
+ >
+
+ {name}
+
+ );
+ },
+ },
+ {
+ field: 'timestamp',
+ name: i18n.translate(
+ 'xpack.securitySolution.endpoint.resolver.panel.table.row.timestampTitle',
+ {
+ defaultMessage: 'Timestamp',
+ }
+ ),
+ dataType: 'date',
+ sortable: true,
+ render(eventDate?: Date) {
+ return eventDate ? (
+ formatter.format(eventDate)
+ ) : (
+
+ {i18n.translate(
+ 'xpack.securitySolution.endpoint.resolver.panel.table.row.timestampInvalidLabel',
+ {
+ defaultMessage: 'invalid',
+ }
+ )}
+
+ );
+ },
+ },
+ ],
+ [pushToQueryParams, handleBringIntoViewClick, isProcessTerminated]
+ );
+
+ const { processNodePositions } = useSelector(selectors.layout);
+ const processTableView: ProcessTableView[] = useMemo(
+ () =>
+ [...processNodePositions.keys()].map((processEvent) => {
+ let dateTime;
+ const eventTime = event.timestampSafeVersion(processEvent);
+ const name = event.processNameSafeVersion(processEvent);
+ if (eventTime) {
+ const date = new Date(eventTime);
+ if (isFinite(date.getTime())) {
+ dateTime = date;
+ }
+ }
+ return {
+ name,
+ timestamp: dateTime,
+ event: processEvent,
+ };
+ }),
+ [processNodePositions]
+ );
+ const numberOfProcesses = processTableView.length;
+
+ const crumbs = useMemo(() => {
+ return [
+ {
+ text: i18n.translate(
+ 'xpack.securitySolution.endpoint.resolver.panel.processListWithCounts.events',
+ {
+ defaultMessage: 'All Process Events',
+ }
+ ),
+ onClick: () => {},
+ },
+ ];
+ }, []);
+
+ const children = useSelector(selectors.hasMoreChildren);
+ const ancestors = useSelector(selectors.hasMoreAncestors);
+ const showWarning = children === true || ancestors === true;
+ return (
+ <>
+
+ {showWarning && }
+
+
+ data-test-subj="resolver:panel:process-list"
+ items={processTableView}
+ columns={columns}
+ sorting
+ />
+ >
+ );
+});
+ProcessListWithCounts.displayName = 'ProcessListWithCounts';
diff --git a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx
index 24de45ee894dc..2a5d91028d9f5 100644
--- a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx
+++ b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx
@@ -14,10 +14,9 @@ import { NodeSubMenu, subMenuAssets } from './submenu';
import { applyMatrix3 } from '../models/vector2';
import { Vector2, Matrix3 } from '../types';
import { SymbolIds, useResolverTheme, calculateResolverFontSize } from './assets';
-import { ResolverEvent } from '../../../common/endpoint/types';
+import { ResolverEvent, SafeResolverEvent } from '../../../common/endpoint/types';
import { useResolverDispatch } from './use_resolver_dispatch';
import * as eventModel from '../../../common/endpoint/models/event';
-import * as processEventModel from '../models/process_event';
import * as selectors from '../store/selectors';
import { useResolverQueryParams } from './use_resolver_query_params';
@@ -85,7 +84,7 @@ const UnstyledProcessEventDot = React.memo(
/**
* An event which contains details about the process node.
*/
- event: ResolverEvent;
+ event: SafeResolverEvent;
/**
* projectionMatrix which can be used to convert `position` to screen coordinates.
*/
@@ -114,7 +113,11 @@ const UnstyledProcessEventDot = React.memo(
// Node (html id=) IDs
const ariaActiveDescendant = useSelector(selectors.ariaActiveDescendant);
const selectedNode = useSelector(selectors.selectedNode);
- const nodeID = processEventModel.uniquePidForProcess(event);
+ const nodeID: string | undefined = eventModel.entityIDSafeVersion(event);
+ if (nodeID === undefined) {
+ // NB: this component should be taking nodeID as a `string` instead of handling this logic here
+ throw new Error('Tried to render a node with no ID');
+ }
const relatedEventStats = useSelector(selectors.relatedEventsStats)(nodeID);
// define a standard way of giving HTML IDs to nodes based on their entity_id/nodeID.
@@ -287,7 +290,9 @@ const UnstyledProcessEventDot = React.memo(
? subMenuAssets.initialMenuStatus
: relatedEventOptions;
- const grandTotal: number | null = useSelector(selectors.relatedEventTotalForProcess)(event);
+ const grandTotal: number | null = useSelector(selectors.relatedEventTotalForProcess)(
+ event as ResolverEvent
+ );
/* eslint-disable jsx-a11y/click-events-have-key-events */
/**
@@ -398,11 +403,11 @@ const UnstyledProcessEventDot = React.memo(
maxWidth: `${isShowingEventActions ? 400 : 210 * xScale}px`,
}}
tabIndex={-1}
- title={eventModel.eventName(event)}
+ title={eventModel.processNameSafeVersion(event)}
>
- {eventModel.eventName(event)}
+ {eventModel.processNameSafeVersion(event)}
diff --git a/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx b/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx
index e74502243ffc8..5f1e5f18e575d 100644
--- a/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx
+++ b/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx
@@ -20,7 +20,7 @@ import { SymbolDefinitions, useResolverTheme } from './assets';
import { useStateSyncingActions } from './use_state_syncing_actions';
import { useResolverQueryParams } from './use_resolver_query_params';
import { StyledMapContainer, StyledPanel, GraphContainer } from './styles';
-import { entityId } from '../../../common/endpoint/models/event';
+import { entityIDSafeVersion } from '../../../common/endpoint/models/event';
import { SideEffectContext } from './side_effect_context';
import { ResolverProps } from '../types';
@@ -114,7 +114,7 @@ export const ResolverWithoutProviders = React.memo(
)
)}
{[...processNodePositions].map(([processEvent, position]) => {
- const processEntityId = entityId(processEvent);
+ const processEntityId = entityIDSafeVersion(processEvent);
return (
{count ? : ''} {menuTitle}
diff --git a/x-pack/plugins/security_solution/public/resolver/view/use_camera.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/use_camera.test.tsx
index b32d63283b547..630ee2f7ff7f0 100644
--- a/x-pack/plugins/security_solution/public/resolver/view/use_camera.test.tsx
+++ b/x-pack/plugins/security_solution/public/resolver/view/use_camera.test.tsx
@@ -191,7 +191,7 @@ describe('useCamera on an unpainted element', () => {
}
const processes: ResolverEvent[] = [
...selectors.layout(store.getState()).processNodePositions.keys(),
- ];
+ ] as ResolverEvent[];
process = processes[processes.length - 1];
if (!process) {
throw new Error('missing the process to bring into view');
diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts
index 0a08e45324b89..c2e23cc19d89e 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts
+++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts
@@ -363,6 +363,7 @@ export const queryTimelineById = ({
export const dispatchUpdateTimeline = (dispatch: Dispatch): DispatchUpdateTimeline => ({
duplicate,
id,
+ forceNotes = false,
from,
notes,
timeline,
@@ -407,7 +408,7 @@ export const dispatchUpdateTimeline = (dispatch: Dispatch): DispatchUpdateTimeli
dispatch(dispatchAddGlobalTimelineNote({ noteId: newNote.id, id }));
}
- if (!duplicate) {
+ if (!duplicate || forceNotes) {
dispatch(
dispatchAddNotes({
notes:
diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts
index 8950f814d6965..769a0a1658a46 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts
+++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts
@@ -192,6 +192,7 @@ export interface OpenTimelineProps {
export interface UpdateTimeline {
duplicate: boolean;
id: string;
+ forceNotes?: boolean;
from: string;
notes: NoteResult[] | null | undefined;
timeline: TimelineModel;
diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts
index c20aaed10f3f8..9d15b4464c191 100644
--- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts
@@ -237,7 +237,7 @@ export class ManifestManager {
const { items, total } = await this.packageConfigService.list(this.savedObjectsClient, {
page,
perPage: 20,
- kuery: 'ingest-package-configs.package.name:endpoint',
+ kuery: 'ingest-package-policies.package.name:endpoint',
});
for (const packageConfig of items) {
diff --git a/x-pack/plugins/security_solution/server/lib/hosts/mock.ts b/x-pack/plugins/security_solution/server/lib/hosts/mock.ts
index 44767563c6b75..97aa68c0f9bbf 100644
--- a/x-pack/plugins/security_solution/server/lib/hosts/mock.ts
+++ b/x-pack/plugins/security_solution/server/lib/hosts/mock.ts
@@ -588,7 +588,7 @@ export const mockEndpointMetadata = {
type: 'endpoint',
version: '7.9.0-SNAPSHOT',
},
- dataset: { name: 'endpoint.metadata', namespace: 'default', type: 'metrics' },
+ data_stream: { dataset: 'endpoint.metadata', namespace: 'default', type: 'metrics' },
ecs: { version: '1.5.0' },
elastic: { agent: { id: '' } },
event: {
diff --git a/x-pack/plugins/security_solution/server/usage/detections/detections.mocks.ts b/x-pack/plugins/security_solution/server/usage/detections/detections.mocks.ts
index e59b1092978da..7afc185ae07fd 100644
--- a/x-pack/plugins/security_solution/server/usage/detections/detections.mocks.ts
+++ b/x-pack/plugins/security_solution/server/usage/detections/detections.mocks.ts
@@ -41,7 +41,7 @@ export const getMockJobSummaryResponse = () => [
{
id: 'other_job',
description: 'a job that is custom',
- groups: ['auditbeat', 'process'],
+ groups: ['auditbeat', 'process', 'security'],
processed_record_count: 0,
memory_status: 'ok',
jobState: 'closed',
@@ -54,6 +54,19 @@ export const getMockJobSummaryResponse = () => [
{
id: 'another_job',
description: 'another job that is custom',
+ groups: ['auditbeat', 'process', 'security'],
+ processed_record_count: 0,
+ memory_status: 'ok',
+ jobState: 'opened',
+ hasDatafeed: true,
+ datafeedId: 'datafeed-another',
+ datafeedIndices: ['auditbeat-*'],
+ datafeedState: 'started',
+ isSingleMetricViewerJob: true,
+ },
+ {
+ id: 'irrelevant_job',
+ description: 'a non-security job',
groups: ['auditbeat', 'process'],
processed_record_count: 0,
memory_status: 'ok',
diff --git a/x-pack/plugins/security_solution/server/usage/detections/detections_helpers.ts b/x-pack/plugins/security_solution/server/usage/detections/detections_helpers.ts
index 80a9dba26df8e..a6d4dc7a38e14 100644
--- a/x-pack/plugins/security_solution/server/usage/detections/detections_helpers.ts
+++ b/x-pack/plugins/security_solution/server/usage/detections/detections_helpers.ts
@@ -15,6 +15,7 @@ import { MlPluginSetup } from '../../../../ml/server';
import { SIGNALS_ID, INTERNAL_IMMUTABLE_KEY } from '../../../common/constants';
import { DetectionRulesUsage, MlJobsUsage } from './index';
import { isJobStarted } from '../../../common/machine_learning/helpers';
+import { isSecurityJob } from '../../../common/machine_learning/is_security_job';
interface DetectionsMetric {
isElastic: boolean;
@@ -182,11 +183,9 @@ export const getMlJobsUsage = async (ml: MlPluginSetup | undefined): Promise module.jobs);
- const jobs = await ml
- .jobServiceProvider(internalMlClient, fakeRequest)
- .jobsSummary(['siem', 'security']);
+ const jobs = await ml.jobServiceProvider(internalMlClient, fakeRequest).jobsSummary();
- jobsUsage = jobs.reduce((usage, job) => {
+ jobsUsage = jobs.filter(isSecurityJob).reduce((usage, job) => {
const isElastic = moduleJobs.some((moduleJob) => moduleJob.id === job.id);
const isEnabled = isJobStarted(job.jobState, job.datafeedState);
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index 8218904f77df9..c2f180f5268d4 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -8207,7 +8207,6 @@
"xpack.indexLifecycleMgmt.editPolicy.nodeAttributesMissingDescription": "ノード属性なしではシャードの割り当てをコントロールできません。",
"xpack.indexLifecycleMgmt.editPolicy.nodeAttributesMissingLabel": "elasticsearch.yml でノード属性が構成されていません",
"xpack.indexLifecycleMgmt.editPolicy.nodeDetailErrorMessage": "ノード属性の詳細の読み込み中にエラーが発生しました",
- "xpack.indexLifecycleMgmt.editPolicy.nodeInfoErrorMessage": "ノード属性の情報の読み込み中にエラーが発生しました",
"xpack.indexLifecycleMgmt.editPolicy.numberRequiredError": "数字が必要です。",
"xpack.indexLifecycleMgmt.editPolicy.phaseCold.minimumAgeLabel": "コールドフェーズのタイミング",
"xpack.indexLifecycleMgmt.editPolicy.phaseCold.minimumAgeUnitsAriaLabel": "コールドフェーズのタイミングの単位",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 21a42362bcdd3..84c3eab8db9e7 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -8209,7 +8209,6 @@
"xpack.indexLifecycleMgmt.editPolicy.nodeAttributesMissingDescription": "没有节点属性,将无法控制分片分配。",
"xpack.indexLifecycleMgmt.editPolicy.nodeAttributesMissingLabel": "elasticsearch.yml 中未配置任何节点属性",
"xpack.indexLifecycleMgmt.editPolicy.nodeDetailErrorMessage": "加载节点属性详细信息时出错",
- "xpack.indexLifecycleMgmt.editPolicy.nodeInfoErrorMessage": "加载节点属性信息时出错",
"xpack.indexLifecycleMgmt.editPolicy.numberRequiredError": "数字必填。",
"xpack.indexLifecycleMgmt.editPolicy.phaseCold.minimumAgeLabel": "冷阶段计时",
"xpack.indexLifecycleMgmt.editPolicy.phaseCold.minimumAgeUnitsAriaLabel": "冷阶段计时单位",
diff --git a/x-pack/test/api_integration/apis/lens/telemetry.ts b/x-pack/test/api_integration/apis/lens/telemetry.ts
index 2c05b02cf470f..0ae4753cd2967 100644
--- a/x-pack/test/api_integration/apis/lens/telemetry.ts
+++ b/x-pack/test/api_integration/apis/lens/telemetry.ts
@@ -191,8 +191,9 @@ export default ({ getService }: FtrProviderContext) => {
expect(results.saved_overall).to.eql({
lnsMetric: 1,
bar_stacked: 1,
+ lnsPie: 1,
});
- expect(results.saved_overall_total).to.eql(2);
+ expect(results.saved_overall_total).to.eql(3);
await esArchiver.unload('lens/basic');
});
diff --git a/x-pack/test/functional/apps/dashboard/drilldowns/index.ts b/x-pack/test/functional/apps/dashboard/drilldowns/index.ts
index 4cdb33c06947f..ff604b18e1d51 100644
--- a/x-pack/test/functional/apps/dashboard/drilldowns/index.ts
+++ b/x-pack/test/functional/apps/dashboard/drilldowns/index.ts
@@ -24,6 +24,9 @@ export default function ({ loadTestFile, getService }: FtrProviderContext) {
loadTestFile(require.resolve('./dashboard_drilldowns'));
loadTestFile(require.resolve('./explore_data_panel_action'));
- loadTestFile(require.resolve('./explore_data_chart_action'));
+
+ // Disabled for now as it requires xpack.discoverEnhanced.actions.exploreDataInChart.enabled
+ // setting set in kibana.yml to work. Once that is enabled by default, we can re-enable this test suite.
+ // loadTestFile(require.resolve('./explore_data_chart_action'));
});
}
diff --git a/x-pack/test/functional/apps/dashboard_mode/dashboard_empty_screen.js b/x-pack/test/functional/apps/dashboard_mode/dashboard_empty_screen.js
index 62e07a08d1762..bd35374643e9b 100644
--- a/x-pack/test/functional/apps/dashboard_mode/dashboard_empty_screen.js
+++ b/x-pack/test/functional/apps/dashboard_mode/dashboard_empty_screen.js
@@ -27,7 +27,7 @@ export default function ({ getPageObjects, getService }) {
await PageObjects.dashboard.gotoDashboardLandingPage();
});
- async function createAndAddLens(title) {
+ async function createAndAddLens(title, saveAsNew = false, redirectToOrigin = true) {
log.debug(`createAndAddLens(${title})`);
const inViewMode = await PageObjects.dashboard.getIsInViewMode();
if (inViewMode) {
@@ -52,7 +52,7 @@ export default function ({ getPageObjects, getService }) {
operation: 'terms',
field: 'ip',
});
- await PageObjects.lens.save(title, false, true);
+ await PageObjects.lens.save(title, saveAsNew, redirectToOrigin);
}
it('adds Lens visualization to empty dashboard', async () => {
@@ -100,6 +100,8 @@ export default function ({ getPageObjects, getService }) {
});
it('loses originatingApp connection after save as when redirectToOrigin is false', async () => {
+ await PageObjects.dashboard.saveDashboard('empty dashboard test');
+ await PageObjects.dashboard.switchToEditMode();
const newTitle = 'wowee, my title just got cooler again';
await PageObjects.dashboard.waitForRenderComplete();
await dashboardPanelActions.openContextMenu();
@@ -108,5 +110,17 @@ export default function ({ getPageObjects, getService }) {
await PageObjects.lens.notLinkedToOriginatingApp();
await PageObjects.common.navigateToApp('dashboard');
});
+
+ it('loses originatingApp connection after first save when redirectToOrigin is false', async () => {
+ const title = 'non-dashboard Test Lens';
+ await PageObjects.dashboard.loadSavedDashboard('empty dashboard test');
+ await PageObjects.dashboard.switchToEditMode();
+ await testSubjects.exists('dashboardAddNewPanelButton');
+ await testSubjects.click('dashboardAddNewPanelButton');
+ await dashboardVisualizations.ensureNewVisualizationDialogIsShowing();
+ await createAndAddLens(title, false, false);
+ await PageObjects.lens.notLinkedToOriginatingApp();
+ await PageObjects.common.navigateToApp('dashboard');
+ });
});
}
diff --git a/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts b/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts
index 767dad74c23d7..f8dc2f3b0aeb8 100644
--- a/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts
+++ b/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts
@@ -137,7 +137,10 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
});
});
- describe('space with index pattern management disabled', () => {
+ describe('space with index pattern management disabled', function () {
+ // unskipped because of flakiness in cloud, caused be ingest management tests
+ // should be unskipped when https://github.com/elastic/kibana/issues/74353 was resolved
+ this.tags(['skipCloud']);
before(async () => {
await spacesService.create({
id: 'custom_space_no_index_patterns',
diff --git a/x-pack/test/functional/apps/lens/dashboard.ts b/x-pack/test/functional/apps/lens/dashboard.ts
new file mode 100644
index 0000000000000..ccf2f88a9d0ed
--- /dev/null
+++ b/x-pack/test/functional/apps/lens/dashboard.ts
@@ -0,0 +1,64 @@
+/*
+ * 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 expect from '@kbn/expect';
+import { FtrProviderContext } from '../../ftr_provider_context';
+
+export default function ({ getService, getPageObjects }: FtrProviderContext) {
+ const PageObjects = getPageObjects(['header', 'common', 'dashboard', 'timePicker', 'lens']);
+
+ const find = getService('find');
+ const dashboardAddPanel = getService('dashboardAddPanel');
+ const elasticChart = getService('elasticChart');
+ const browser = getService('browser');
+ const retry = getService('retry');
+ const testSubjects = getService('testSubjects');
+ const filterBar = getService('filterBar');
+
+ async function clickInChart(x: number, y: number) {
+ const el = await elasticChart.getCanvas();
+ await browser.getActions().move({ x, y, origin: el._webElement }).click().perform();
+ }
+
+ describe('lens dashboard tests', () => {
+ it('metric should be embeddable', async () => {
+ await PageObjects.common.navigateToApp('dashboard');
+ await PageObjects.dashboard.clickNewDashboard();
+ await dashboardAddPanel.clickOpenAddPanel();
+ await dashboardAddPanel.filterEmbeddableNames('Artistpreviouslyknownaslens');
+ await find.clickByButtonText('Artistpreviouslyknownaslens');
+ await dashboardAddPanel.closeAddPanel();
+ await PageObjects.lens.goToTimeRange();
+ await PageObjects.lens.assertMetric('Maximum of bytes', '19,986');
+ });
+
+ it('should be able to add filters/timerange by clicking in XYChart', async () => {
+ await PageObjects.common.navigateToApp('dashboard');
+ await PageObjects.dashboard.clickNewDashboard();
+ await dashboardAddPanel.clickOpenAddPanel();
+ await dashboardAddPanel.filterEmbeddableNames('lnsXYvis');
+ await find.clickByButtonText('lnsXYvis');
+ await dashboardAddPanel.closeAddPanel();
+ await PageObjects.lens.goToTimeRange();
+ await clickInChart(5, 5); // hardcoded position of bar
+
+ await retry.try(async () => {
+ await testSubjects.click('applyFiltersPopoverButton');
+ await testSubjects.missingOrFail('applyFiltersPopoverButton');
+ });
+
+ await PageObjects.lens.assertExactText(
+ '[data-test-subj="embeddablePanelHeading-lnsXYvis"]',
+ 'lnsXYvis'
+ );
+ const time = await PageObjects.timePicker.getTimeConfig();
+ expect(time.start).to.equal('Sep 21, 2015 @ 09:00:00.000');
+ expect(time.end).to.equal('Sep 21, 2015 @ 12:00:00.000');
+ const hasIpFilter = await filterBar.hasFilter('ip', '97.220.3.248');
+ expect(hasIpFilter).to.be(true);
+ });
+ });
+}
diff --git a/x-pack/test/functional/apps/lens/index.ts b/x-pack/test/functional/apps/lens/index.ts
index b17b7d856841c..f2dcf28c01743 100644
--- a/x-pack/test/functional/apps/lens/index.ts
+++ b/x-pack/test/functional/apps/lens/index.ts
@@ -28,6 +28,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) {
this.tags(['ciGroup4', 'skipFirefox']);
loadTestFile(require.resolve('./smokescreen'));
+ loadTestFile(require.resolve('./dashboard'));
loadTestFile(require.resolve('./persistent_context'));
loadTestFile(require.resolve('./lens_reporting'));
});
diff --git a/x-pack/test/functional/apps/lens/smokescreen.ts b/x-pack/test/functional/apps/lens/smokescreen.ts
index 1e93636161067..77b9aa1e25edd 100644
--- a/x-pack/test/functional/apps/lens/smokescreen.ts
+++ b/x-pack/test/functional/apps/lens/smokescreen.ts
@@ -8,115 +8,20 @@ import expect from '@kbn/expect';
import { FtrProviderContext } from '../../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
- const PageObjects = getPageObjects([
- 'header',
- 'common',
- 'visualize',
- 'dashboard',
- 'header',
- 'timePicker',
- 'lens',
- ]);
+ const PageObjects = getPageObjects(['visualize', 'lens']);
const find = getService('find');
- const dashboardAddPanel = getService('dashboardAddPanel');
- const elasticChart = getService('elasticChart');
- const browser = getService('browser');
- const retry = getService('retry');
- const testSubjects = getService('testSubjects');
- const filterBar = getService('filterBar');
const listingTable = getService('listingTable');
- async function assertExpectedMetric(metricCount: string = '19,986') {
- await PageObjects.lens.assertExactText(
- '[data-test-subj="lns_metric_title"]',
- 'Maximum of bytes'
- );
- await PageObjects.lens.assertExactText('[data-test-subj="lns_metric_value"]', metricCount);
- }
-
- async function assertExpectedTable() {
- await PageObjects.lens.assertExactText(
- '[data-test-subj="lnsDataTable"] thead .euiTableCellContent__text',
- 'Maximum of bytes'
- );
- await PageObjects.lens.assertExactText(
- '[data-test-subj="lnsDataTable"] [data-test-subj="lnsDataTableCellValue"]',
- '19,986'
- );
- }
-
- async function assertExpectedChart() {
- await PageObjects.lens.assertExactText(
- '[data-test-subj="embeddablePanelHeading-lnsXYvis"]',
- 'lnsXYvis'
- );
- }
-
- async function assertExpectedTimerange() {
- const time = await PageObjects.timePicker.getTimeConfig();
- expect(time.start).to.equal('Sep 21, 2015 @ 09:00:00.000');
- expect(time.end).to.equal('Sep 21, 2015 @ 12:00:00.000');
- }
-
- async function clickOnBarHistogram() {
- const el = await elasticChart.getCanvas();
- await browser.getActions().move({ x: 5, y: 5, origin: el._webElement }).click().perform();
- }
-
describe('lens smokescreen tests', () => {
it('should allow editing saved visualizations', async () => {
await PageObjects.visualize.gotoVisualizationLandingPage();
await listingTable.searchForItemWithName('Artistpreviouslyknownaslens');
await PageObjects.lens.clickVisualizeListItemTitle('Artistpreviouslyknownaslens');
await PageObjects.lens.goToTimeRange();
- await assertExpectedMetric();
- });
-
- it('metric should be embeddable in dashboards', async () => {
- await PageObjects.common.navigateToApp('dashboard');
- await PageObjects.dashboard.clickNewDashboard();
- await dashboardAddPanel.clickOpenAddPanel();
- await dashboardAddPanel.filterEmbeddableNames('Artistpreviouslyknownaslens');
- await find.clickByButtonText('Artistpreviouslyknownaslens');
- await dashboardAddPanel.closeAddPanel();
- await PageObjects.lens.goToTimeRange();
- await assertExpectedMetric();
+ await PageObjects.lens.assertMetric('Maximum of bytes', '19,986');
});
- it('click on the bar in XYChart adds proper filters/timerange in dashboard', async () => {
- await PageObjects.common.navigateToApp('dashboard');
- await PageObjects.dashboard.clickNewDashboard();
- await dashboardAddPanel.clickOpenAddPanel();
- await dashboardAddPanel.filterEmbeddableNames('lnsXYvis');
- await find.clickByButtonText('lnsXYvis');
- await dashboardAddPanel.closeAddPanel();
- await PageObjects.lens.goToTimeRange();
- await clickOnBarHistogram();
-
- await retry.try(async () => {
- await testSubjects.click('applyFiltersPopoverButton');
- await testSubjects.missingOrFail('applyFiltersPopoverButton');
- });
-
- await assertExpectedChart();
- await assertExpectedTimerange();
- const hasIpFilter = await filterBar.hasFilter('ip', '97.220.3.248');
- expect(hasIpFilter).to.be(true);
- });
-
- it('should allow seamless transition to and from table view', async () => {
- await PageObjects.visualize.gotoVisualizationLandingPage();
- await listingTable.searchForItemWithName('Artistpreviouslyknownaslens');
- await PageObjects.lens.clickVisualizeListItemTitle('Artistpreviouslyknownaslens');
- await PageObjects.lens.goToTimeRange();
- await assertExpectedMetric();
- await PageObjects.lens.switchToVisualization('lnsDatatable');
- await assertExpectedTable();
- await PageObjects.lens.switchToVisualization('lnsMetric');
- await assertExpectedMetric();
- });
-
- it('should allow creation of lens visualizations', async () => {
+ it('should allow creation of lens xy chart', async () => {
await PageObjects.visualize.navigateToNewVisualization();
await PageObjects.visualize.clickVisType('lens');
await PageObjects.lens.goToTimeRange();
@@ -165,6 +70,19 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
expect(await find.allByCssSelector('.echLegendItem')).to.have.length(3);
});
+ it('should allow seamless transition to and from table view', async () => {
+ await PageObjects.visualize.gotoVisualizationLandingPage();
+ await listingTable.searchForItemWithName('Artistpreviouslyknownaslens');
+ await PageObjects.lens.clickVisualizeListItemTitle('Artistpreviouslyknownaslens');
+ await PageObjects.lens.goToTimeRange();
+ await PageObjects.lens.assertMetric('Maximum of bytes', '19,986');
+ await PageObjects.lens.switchToVisualization('lnsDatatable');
+ expect(await PageObjects.lens.getDatatableHeaderText()).to.eql('Maximum of bytes');
+ expect(await PageObjects.lens.getDatatableCellText(0, 0)).to.eql('19,986');
+ await PageObjects.lens.switchToVisualization('lnsMetric');
+ await PageObjects.lens.assertMetric('Maximum of bytes', '19,986');
+ });
+
it('should switch from a multi-layer stacked bar to a multi-layer line chart', async () => {
await PageObjects.visualize.navigateToNewVisualization();
await PageObjects.visualize.clickVisType('lens');
@@ -190,5 +108,94 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
expect(await PageObjects.lens.getLayerCount()).to.eql(2);
});
+
+ it('should allow transition from line chart to donut chart and to bar chart', async () => {
+ await PageObjects.visualize.gotoVisualizationLandingPage();
+ await listingTable.searchForItemWithName('lnsXYvis');
+ await PageObjects.lens.clickVisualizeListItemTitle('lnsXYvis');
+ await PageObjects.lens.goToTimeRange();
+ expect(await PageObjects.lens.hasChartSwitchWarning('donut')).to.eql(true);
+ await PageObjects.lens.switchToVisualization('donut');
+
+ expect(await PageObjects.lens.getTitle()).to.eql('lnsXYvis');
+ expect(await PageObjects.lens.getDimensionTriggerText('lnsPie_sliceByDimensionPanel')).to.eql(
+ 'Top values of ip'
+ );
+ expect(await PageObjects.lens.getDimensionTriggerText('lnsPie_sizeByDimensionPanel')).to.eql(
+ 'Average of bytes'
+ );
+
+ expect(await PageObjects.lens.hasChartSwitchWarning('bar')).to.eql(false);
+ await PageObjects.lens.switchToVisualization('bar');
+ expect(await PageObjects.lens.getTitle()).to.eql('lnsXYvis');
+ expect(await PageObjects.lens.getDimensionTriggerText('lnsXY_xDimensionPanel')).to.eql(
+ 'Top values of ip'
+ );
+ expect(await PageObjects.lens.getDimensionTriggerText('lnsXY_yDimensionPanel')).to.eql(
+ 'Average of bytes'
+ );
+ });
+
+ it('should allow seamless transition from bar chart to line chart using layer chart switch', async () => {
+ await PageObjects.visualize.gotoVisualizationLandingPage();
+ await listingTable.searchForItemWithName('lnsXYvis');
+ await PageObjects.lens.clickVisualizeListItemTitle('lnsXYvis');
+ await PageObjects.lens.goToTimeRange();
+ await PageObjects.lens.switchLayerSeriesType('line');
+ expect(await PageObjects.lens.getTitle()).to.eql('lnsXYvis');
+ expect(await PageObjects.lens.getDimensionTriggerText('lnsXY_xDimensionPanel')).to.eql(
+ '@timestamp'
+ );
+ expect(await PageObjects.lens.getDimensionTriggerText('lnsXY_yDimensionPanel')).to.eql(
+ 'Average of bytes'
+ );
+ expect(await PageObjects.lens.getDimensionTriggerText('lnsXY_splitDimensionPanel')).to.eql(
+ 'Top values of ip'
+ );
+ });
+
+ it('should allow seamless transition from pie chart to treemap chart', async () => {
+ await PageObjects.visualize.gotoVisualizationLandingPage();
+ await listingTable.searchForItemWithName('lnsPieVis');
+ await PageObjects.lens.clickVisualizeListItemTitle('lnsPieVis');
+ await PageObjects.lens.goToTimeRange();
+ expect(await PageObjects.lens.hasChartSwitchWarning('treemap')).to.eql(false);
+ await PageObjects.lens.switchToVisualization('treemap');
+ expect(
+ await PageObjects.lens.getDimensionTriggerText('lnsPie_groupByDimensionPanel', 0)
+ ).to.eql('Top values of geo.dest');
+ expect(
+ await PageObjects.lens.getDimensionTriggerText('lnsPie_groupByDimensionPanel', 1)
+ ).to.eql('Top values of geo.src');
+ expect(await PageObjects.lens.getDimensionTriggerText('lnsPie_sizeByDimensionPanel')).to.eql(
+ 'Average of bytes'
+ );
+ });
+
+ it('should allow creating a pie chart and switching to datatable', async () => {
+ await PageObjects.visualize.navigateToNewVisualization();
+ await PageObjects.visualize.clickVisType('lens');
+ await PageObjects.lens.goToTimeRange();
+ await PageObjects.lens.switchToVisualization('pie');
+ await PageObjects.lens.configureDimension({
+ dimension: 'lnsPie_sliceByDimensionPanel > lns-empty-dimension',
+ operation: 'date_histogram',
+ field: '@timestamp',
+ });
+
+ await PageObjects.lens.configureDimension({
+ dimension: 'lnsPie_sizeByDimensionPanel > lns-empty-dimension',
+ operation: 'avg',
+ field: 'bytes',
+ });
+
+ expect(await PageObjects.lens.hasChartSwitchWarning('lnsDatatable')).to.eql(false);
+ await PageObjects.lens.switchToVisualization('lnsDatatable');
+
+ expect(await PageObjects.lens.getDatatableHeaderText()).to.eql('@timestamp per 3 hours');
+ expect(await PageObjects.lens.getDatatableCellText(0, 0)).to.eql('2015-09-20 00:00');
+ expect(await PageObjects.lens.getDatatableHeaderText(1)).to.eql('Average of bytes');
+ expect(await PageObjects.lens.getDatatableCellText(0, 1)).to.eql('6,011.351');
+ });
});
}
diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation.ts
index 4566e9aed61b4..a62bfdcde0572 100644
--- a/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation.ts
+++ b/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation.ts
@@ -10,6 +10,7 @@ import { FtrProviderContext } from '../../../ftr_provider_context';
export default function ({ getService }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const ml = getService('ml');
+ const editedDescription = 'Edited description';
describe('classification creation', function () {
before(async () => {
@@ -179,6 +180,36 @@ export default function ({ getService }: FtrProviderContext) {
});
});
+ it('should open the edit form for the created job in the analytics table', async () => {
+ await ml.dataFrameAnalyticsTable.openEditFlyout(testData.jobId);
+ });
+
+ it('should input the description in the edit form', async () => {
+ await ml.dataFrameAnalyticsEdit.assertJobDescriptionEditInputExists();
+ await ml.dataFrameAnalyticsEdit.setJobDescriptionEdit(editedDescription);
+ });
+
+ it('should input the model memory limit in the edit form', async () => {
+ await ml.dataFrameAnalyticsEdit.assertJobMmlEditInputExists();
+ await ml.dataFrameAnalyticsEdit.setJobMmlEdit('21mb');
+ });
+
+ it('should submit the edit job form', async () => {
+ await ml.dataFrameAnalyticsEdit.updateAnalyticsJob();
+ });
+
+ it('displays details for the edited job in the analytics table', async () => {
+ await ml.dataFrameAnalyticsTable.assertAnalyticsRowFields(testData.jobId, {
+ id: testData.jobId,
+ description: editedDescription,
+ sourceIndex: testData.source,
+ destinationIndex: testData.destinationIndex,
+ type: testData.expected.row.type,
+ status: testData.expected.row.status,
+ progress: testData.expected.row.progress,
+ });
+ });
+
it('creates the destination index and writes results to it', async () => {
await ml.api.assertIndicesExist(testData.destinationIndex);
await ml.api.assertIndicesNotEmpty(testData.destinationIndex);
diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation.ts
index 0320354b99ff0..5b89cec49db3e 100644
--- a/x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation.ts
+++ b/x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation.ts
@@ -10,6 +10,7 @@ import { FtrProviderContext } from '../../../ftr_provider_context';
export default function ({ getService }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const ml = getService('ml');
+ const editedDescription = 'Edited description';
describe('outlier detection creation', function () {
before(async () => {
@@ -197,6 +198,36 @@ export default function ({ getService }: FtrProviderContext) {
});
});
+ it('should open the edit form for the created job in the analytics table', async () => {
+ await ml.dataFrameAnalyticsTable.openEditFlyout(testData.jobId);
+ });
+
+ it('should input the description in the edit form', async () => {
+ await ml.dataFrameAnalyticsEdit.assertJobDescriptionEditInputExists();
+ await ml.dataFrameAnalyticsEdit.setJobDescriptionEdit(editedDescription);
+ });
+
+ it('should input the model memory limit in the edit form', async () => {
+ await ml.dataFrameAnalyticsEdit.assertJobMmlEditInputExists();
+ await ml.dataFrameAnalyticsEdit.setJobMmlEdit('21mb');
+ });
+
+ it('should submit the edit job form', async () => {
+ await ml.dataFrameAnalyticsEdit.updateAnalyticsJob();
+ });
+
+ it('displays details for the edited job in the analytics table', async () => {
+ await ml.dataFrameAnalyticsTable.assertAnalyticsRowFields(testData.jobId, {
+ id: testData.jobId,
+ description: editedDescription,
+ sourceIndex: testData.source,
+ destinationIndex: testData.destinationIndex,
+ type: testData.expected.row.type,
+ status: testData.expected.row.status,
+ progress: testData.expected.row.progress,
+ });
+ });
+
it('creates the destination index and writes results to it', async () => {
await ml.api.assertIndicesExist(testData.destinationIndex);
await ml.api.assertIndicesNotEmpty(testData.destinationIndex);
diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts
index 1aa505e26e1e9..a67a348323347 100644
--- a/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts
+++ b/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts
@@ -10,6 +10,7 @@ import { FtrProviderContext } from '../../../ftr_provider_context';
export default function ({ getService }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const ml = getService('ml');
+ const editedDescription = 'Edited description';
describe('regression creation', function () {
before(async () => {
@@ -179,6 +180,36 @@ export default function ({ getService }: FtrProviderContext) {
});
});
+ it('should open the edit form for the created job in the analytics table', async () => {
+ await ml.dataFrameAnalyticsTable.openEditFlyout(testData.jobId);
+ });
+
+ it('should input the description in the edit form', async () => {
+ await ml.dataFrameAnalyticsEdit.assertJobDescriptionEditInputExists();
+ await ml.dataFrameAnalyticsEdit.setJobDescriptionEdit(editedDescription);
+ });
+
+ it('should input the model memory limit in the edit form', async () => {
+ await ml.dataFrameAnalyticsEdit.assertJobMmlEditInputExists();
+ await ml.dataFrameAnalyticsEdit.setJobMmlEdit('21mb');
+ });
+
+ it('should submit the edit job form', async () => {
+ await ml.dataFrameAnalyticsEdit.updateAnalyticsJob();
+ });
+
+ it('displays details for the edited job in the analytics table', async () => {
+ await ml.dataFrameAnalyticsTable.assertAnalyticsRowFields(testData.jobId, {
+ id: testData.jobId,
+ description: editedDescription,
+ sourceIndex: testData.source,
+ destinationIndex: testData.destinationIndex,
+ type: testData.expected.row.type,
+ status: testData.expected.row.status,
+ progress: testData.expected.row.progress,
+ });
+ });
+
it('creates the destination index and writes results to it', async () => {
await ml.api.assertIndicesExist(testData.destinationIndex);
await ml.api.assertIndicesNotEmpty(testData.destinationIndex);
diff --git a/x-pack/test/functional/apps/ml/pages.ts b/x-pack/test/functional/apps/ml/pages.ts
index e2c80c8dab558..3691e6b1afcdc 100644
--- a/x-pack/test/functional/apps/ml/pages.ts
+++ b/x-pack/test/functional/apps/ml/pages.ts
@@ -53,5 +53,17 @@ export default function ({ getService }: FtrProviderContext) {
await ml.dataVisualizer.assertDataVisualizerImportDataCardExists();
await ml.dataVisualizer.assertDataVisualizerIndexDataCardExists();
});
+
+ it('should load the stack management with the ML menu item being present', async () => {
+ await ml.navigation.navigateToStackManagement();
+ });
+
+ it('should load the jobs list page in stack management', async () => {
+ await ml.navigation.navigateToStackManagementJobsListPage();
+ });
+
+ it('should load the analytics jobs list page in stack management', async () => {
+ await ml.navigation.navigateToStackManagementJobsListPageAnalyticsTab();
+ });
});
}
diff --git a/x-pack/test/functional/es_archives/fleet/agents/mappings.json b/x-pack/test/functional/es_archives/fleet/agents/mappings.json
index acc32c3e2cbb5..12d3be3e2a971 100644
--- a/x-pack/test/functional/es_archives/fleet/agents/mappings.json
+++ b/x-pack/test/functional/es_archives/fleet/agents/mappings.json
@@ -28,7 +28,7 @@
"application_usage_transactional": "965839e75f809fefe04f92dc4d99722a",
"action_task_params": "a9d49f184ee89641044be0ca2950fa3a",
"fleet-agent-events": "3231653fafe4ef3196fe3b32ab774bf2",
- "ingest-package-configs": "2346514df03316001d56ed4c8d46fa94",
+ "ingest-package-policies": "2346514df03316001d56ed4c8d46fa94",
"apm-indices": "9bb9b2bf1fa636ed8619cbab5ce6a1dd",
"inventory-view": "5299b67717e96502c77babf1c16fd4d3",
"upgrade-assistant-reindex-operation": "296a89039fc4260292be36b1b005d8f2",
@@ -1834,7 +1834,7 @@
}
}
},
- "ingest-package-configs": {
+ "ingest-package-policies": {
"properties": {
"config_id": {
"type": "keyword"
@@ -1870,9 +1870,9 @@
"config": {
"type": "flattened"
},
- "dataset": {
+ "data_stream": {
"properties": {
- "name": {
+ "dataset": {
"type": "keyword"
},
"type": {
diff --git a/x-pack/test/functional/es_archives/lens/basic/data.json.gz b/x-pack/test/functional/es_archives/lens/basic/data.json.gz
index 4ed7c29f7391e..ddf4a27289dff 100644
Binary files a/x-pack/test/functional/es_archives/lens/basic/data.json.gz and b/x-pack/test/functional/es_archives/lens/basic/data.json.gz differ
diff --git a/x-pack/test/functional/es_archives/lists/mappings.json b/x-pack/test/functional/es_archives/lists/mappings.json
index 2fc1f1a3111a7..ba4e1b276d45e 100644
--- a/x-pack/test/functional/es_archives/lists/mappings.json
+++ b/x-pack/test/functional/es_archives/lists/mappings.json
@@ -70,7 +70,7 @@
"maps-telemetry": "5ef305b18111b77789afefbd36b66171",
"namespace": "2f4316de49999235636386fe51dc06c1",
"cases-user-actions": "32277330ec6b721abe3b846cfd939a71",
- "ingest-package-configs": "48e8bd97e488008e21c0b5a2367b83ad",
+ "ingest-package-policies": "48e8bd97e488008e21c0b5a2367b83ad",
"timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf",
"siem-ui-timeline-pinned-event": "20638091112f0e14f0e443d512301c29",
"config": "c63748b75f39d0c54de12d12c1ccbc20",
@@ -1274,7 +1274,7 @@
}
}
},
- "ingest-package-configs": {
+ "ingest-package-policies": {
"properties": {
"config_id": {
"type": "keyword"
@@ -1310,9 +1310,9 @@
"config": {
"type": "flattened"
},
- "dataset": {
+ "data_stream": {
"properties": {
- "name": {
+ "dataset": {
"type": "keyword"
},
"type": {
diff --git a/x-pack/test/functional/es_archives/reporting/canvas_disallowed_url/mappings.json b/x-pack/test/functional/es_archives/reporting/canvas_disallowed_url/mappings.json
index 1fd338fbb0ffb..2380154277e55 100644
--- a/x-pack/test/functional/es_archives/reporting/canvas_disallowed_url/mappings.json
+++ b/x-pack/test/functional/es_archives/reporting/canvas_disallowed_url/mappings.json
@@ -39,7 +39,7 @@
"index-pattern": "66eccb05066c5a89924f48a9e9736499",
"ingest-agent-policies": "9326f99c977fd2ef5ab24b6336a0675c",
"ingest-outputs": "8aa988c376e65443fefc26f1075e93a3",
- "ingest-package-configs": "48e8bd97e488008e21c0b5a2367b83ad",
+ "ingest-package-policies": "48e8bd97e488008e21c0b5a2367b83ad",
"ingest_manager_settings": "012cf278ec84579495110bb827d1ed09",
"kql-telemetry": "d12a98a6f19a2d273696597547e064ee",
"lens": "d33c68a69ff1e78c9888dedd2164ac22",
@@ -1212,7 +1212,7 @@
}
}
},
- "ingest-package-configs": {
+ "ingest-package-policies": {
"properties": {
"config_id": {
"type": "keyword"
@@ -1246,9 +1246,9 @@
"config": {
"type": "flattened"
},
- "dataset": {
+ "data_stream": {
"properties": {
- "name": {
+ "dataset": {
"type": "keyword"
},
"type": {
diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts
index 79548db0e2630..bed0e3a159e23 100644
--- a/x-pack/test/functional/page_objects/lens_page.ts
+++ b/x-pack/test/functional/page_objects/lens_page.ts
@@ -176,9 +176,26 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont
*/
async hasChartSwitchWarning(subVisualizationId: string) {
await this.openChartSwitchPopover();
-
const element = await testSubjects.find(`lnsChartSwitchPopover_${subVisualizationId}`);
- return await testSubjects.descendantExists('euiKeyPadMenuItem__betaBadgeWrapper', element);
+ return await find.descendantExistsByCssSelector(
+ '.euiKeyPadMenuItem__betaBadgeWrapper',
+ element
+ );
+ },
+
+ /**
+ * Uses the Lens layer switcher to switch seriesType for xy charts.
+ *
+ * @param subVisualizationId - the ID of the sub-visualization to switch to, such as
+ * line,
+ */
+ async switchLayerSeriesType(seriesType: string) {
+ await retry.try(async () => {
+ await testSubjects.click('lns_layer_settings');
+ await testSubjects.exists(`lnsXY_seriesType-${seriesType}`);
+ });
+
+ return await testSubjects.click(`lnsXY_seriesType-${seriesType}`);
},
/**
@@ -205,5 +222,60 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont
await PageObjects.header.waitUntilLoadingHasFinished();
await testSubjects.missingOrFail('lnsApp_saveAndReturnButton');
},
+ /**
+ * Gets label of dimension trigger in dimension panel
+ *
+ * @param dimension - the selector of the dimension
+ */
+ async getDimensionTriggerText(dimension: string, index = 0) {
+ const dimensionElements = await testSubjects.findAll(dimension);
+ const trigger = await testSubjects.findDescendant(
+ 'lns-dimensionTrigger',
+ dimensionElements[index]
+ );
+ return await trigger.getVisibleText();
+ },
+
+ /**
+ * Gets text of the specified datatable header cell
+ *
+ * @param index - index of th element in datatable
+ */
+ async getDatatableHeaderText(index = 0) {
+ return find
+ .byCssSelector(
+ `[data-test-subj="lnsDataTable"] thead th:nth-child(${
+ index + 1
+ }) .euiTableCellContent__text`
+ )
+ .then((el) => el.getVisibleText());
+ },
+
+ /**
+ * Gets text of the specified datatable cell
+ *
+ * @param rowIndex - index of row of the cell
+ * @param colIndex - index of column of the cell
+ */
+ async getDatatableCellText(rowIndex = 0, colIndex = 0) {
+ return find
+ .byCssSelector(
+ `[data-test-subj="lnsDataTable"] tr:nth-child(${rowIndex + 1}) td:nth-child(${
+ colIndex + 1
+ })`
+ )
+ .then((el) => el.getVisibleText());
+ },
+
+ /**
+ * Asserts that metric has expected title and count
+ *
+ * @param title - expected title
+ * @param count - expected count of metric
+ */
+ async assertMetric(title: string, count: string) {
+ await this.assertExactText('[data-test-subj="lns_metric_title"]', title);
+ await this.assertExactText('[data-test-subj="lns_metric_value"]', count);
+ },
});
}
diff --git a/x-pack/test/functional/services/ml/data_frame_analytics_edit.ts b/x-pack/test/functional/services/ml/data_frame_analytics_edit.ts
new file mode 100644
index 0000000000000..fd06dd24d6f8b
--- /dev/null
+++ b/x-pack/test/functional/services/ml/data_frame_analytics_edit.ts
@@ -0,0 +1,71 @@
+/*
+ * 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 expect from '@kbn/expect';
+
+import { FtrProviderContext } from '../../ftr_provider_context';
+import { MlCommon } from './common';
+
+export function MachineLearningDataFrameAnalyticsEditProvider(
+ { getService }: FtrProviderContext,
+ mlCommon: MlCommon
+) {
+ const testSubjects = getService('testSubjects');
+ const retry = getService('retry');
+
+ return {
+ async assertJobDescriptionEditInputExists() {
+ await testSubjects.existOrFail('mlAnalyticsEditFlyoutDescriptionInput');
+ },
+ async assertJobDescriptionEditValue(expectedValue: string) {
+ const actualJobDescription = await testSubjects.getAttribute(
+ 'mlAnalyticsEditFlyoutDescriptionInput',
+ 'value'
+ );
+ expect(actualJobDescription).to.eql(
+ expectedValue,
+ `Job description edit should be '${expectedValue}' (got '${actualJobDescription}')`
+ );
+ },
+ async assertJobMmlEditInputExists() {
+ await testSubjects.existOrFail('mlAnalyticsEditFlyoutmodelMemoryLimitInput');
+ },
+ async assertJobMmlEditValue(expectedValue: string) {
+ const actualMml = await testSubjects.getAttribute(
+ 'mlAnalyticsEditFlyoutmodelMemoryLimitInput',
+ 'value'
+ );
+ expect(actualMml).to.eql(
+ expectedValue,
+ `Job model memory limit edit should be '${expectedValue}' (got '${actualMml}')`
+ );
+ },
+ async setJobDescriptionEdit(jobDescription: string) {
+ await mlCommon.setValueWithChecks('mlAnalyticsEditFlyoutDescriptionInput', jobDescription, {
+ clearWithKeyboard: true,
+ });
+ await this.assertJobDescriptionEditValue(jobDescription);
+ },
+
+ async setJobMmlEdit(mml: string) {
+ await mlCommon.setValueWithChecks('mlAnalyticsEditFlyoutmodelMemoryLimitInput', mml, {
+ clearWithKeyboard: true,
+ });
+ await this.assertJobMmlEditValue(mml);
+ },
+
+ async assertAnalyticsEditFlyoutMissing() {
+ await testSubjects.missingOrFail('mlAnalyticsEditFlyout');
+ },
+
+ async updateAnalyticsJob() {
+ await testSubjects.existOrFail('mlAnalyticsEditFlyoutUpdateButton');
+ await testSubjects.click('mlAnalyticsEditFlyoutUpdateButton');
+ await retry.tryForTime(5000, async () => {
+ await this.assertAnalyticsEditFlyoutMissing();
+ });
+ },
+ };
+}
diff --git a/x-pack/test/functional/services/ml/data_frame_analytics_table.ts b/x-pack/test/functional/services/ml/data_frame_analytics_table.ts
index d315f9eb77210..608a1f2bee3e1 100644
--- a/x-pack/test/functional/services/ml/data_frame_analytics_table.ts
+++ b/x-pack/test/functional/services/ml/data_frame_analytics_table.ts
@@ -88,6 +88,12 @@ export function MachineLearningDataFrameAnalyticsTableProvider({ getService }: F
await testSubjects.existOrFail('mlAnalyticsJobViewButton');
}
+ public async openEditFlyout(analyticsId: string) {
+ await this.openRowActions(analyticsId);
+ await testSubjects.click('mlAnalyticsJobEditButton');
+ await testSubjects.existOrFail('mlAnalyticsEditFlyout', { timeout: 5000 });
+ }
+
async assertAnalyticsSearchInputValue(expectedSearchValue: string) {
const searchBarInput = await this.getAnalyticsSearchInput();
const actualSearchValue = await searchBarInput.getAttribute('value');
diff --git a/x-pack/test/functional/services/ml/index.ts b/x-pack/test/functional/services/ml/index.ts
index fbf31e40a242a..fd36bb0f47f95 100644
--- a/x-pack/test/functional/services/ml/index.ts
+++ b/x-pack/test/functional/services/ml/index.ts
@@ -13,6 +13,7 @@ import { MachineLearningCommonProvider } from './common';
import { MachineLearningCustomUrlsProvider } from './custom_urls';
import { MachineLearningDataFrameAnalyticsProvider } from './data_frame_analytics';
import { MachineLearningDataFrameAnalyticsCreationProvider } from './data_frame_analytics_creation';
+import { MachineLearningDataFrameAnalyticsEditProvider } from './data_frame_analytics_edit';
import { MachineLearningDataFrameAnalyticsTableProvider } from './data_frame_analytics_table';
import { MachineLearningDataVisualizerProvider } from './data_visualizer';
import { MachineLearningDataVisualizerFileBasedProvider } from './data_visualizer_file_based';
@@ -47,6 +48,7 @@ export function MachineLearningProvider(context: FtrProviderContext) {
common,
api
);
+ const dataFrameAnalyticsEdit = MachineLearningDataFrameAnalyticsEditProvider(context, common);
const dataFrameAnalyticsTable = MachineLearningDataFrameAnalyticsTableProvider(context);
const dataVisualizer = MachineLearningDataVisualizerProvider(context);
const dataVisualizerFileBased = MachineLearningDataVisualizerFileBasedProvider(context, common);
@@ -76,6 +78,7 @@ export function MachineLearningProvider(context: FtrProviderContext) {
customUrls,
dataFrameAnalytics,
dataFrameAnalyticsCreation,
+ dataFrameAnalyticsEdit,
dataFrameAnalyticsTable,
dataVisualizer,
dataVisualizerFileBased,
diff --git a/x-pack/test/functional/services/ml/navigation.ts b/x-pack/test/functional/services/ml/navigation.ts
index 9b67a369f055f..f52197d4b2256 100644
--- a/x-pack/test/functional/services/ml/navigation.ts
+++ b/x-pack/test/functional/services/ml/navigation.ts
@@ -23,6 +23,13 @@ export function MachineLearningNavigationProvider({
});
},
+ async navigateToStackManagement() {
+ await retry.tryForTime(60 * 1000, async () => {
+ await PageObjects.common.navigateToApp('management');
+ await testSubjects.existOrFail('jobsListLink', { timeout: 2000 });
+ });
+ },
+
async assertTabsExist(tabTypeSubject: string, areaSubjects: string[]) {
await retry.tryForTime(10000, async () => {
const allTabs = await testSubjects.findAll(`~${tabTypeSubject}`, 3);
@@ -76,5 +83,25 @@ export function MachineLearningNavigationProvider({
async navigateToSettings() {
await this.navigateToArea('~mlMainTab & ~settings', 'mlPageSettings');
},
+
+ async navigateToStackManagementJobsListPage() {
+ // clicks the jobsListLink and loads the jobs list page
+ await testSubjects.click('jobsListLink');
+ await retry.tryForTime(60 * 1000, async () => {
+ // verify that the overall page is present
+ await testSubjects.existOrFail('mlPageStackManagementJobsList');
+ // verify that the default tab with the anomaly detection jobs list got loaded
+ await testSubjects.existOrFail('ml-jobs-list');
+ });
+ },
+
+ async navigateToStackManagementJobsListPageAnalyticsTab() {
+ // clicks the `Analytics` tab and loads the analytics list page
+ await testSubjects.click('mlStackManagementJobsListAnalyticsTab');
+ await retry.tryForTime(60 * 1000, async () => {
+ // verify that the empty prompt for analytics jobs list got loaded
+ await testSubjects.existOrFail('mlNoDataFrameAnalyticsFound');
+ });
+ },
};
}
diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/list.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/list.ts
index b9c38e576ca73..bfe1954e46c9f 100644
--- a/x-pack/test/ingest_manager_api_integration/apis/epm/list.ts
+++ b/x-pack/test/ingest_manager_api_integration/apis/epm/list.ts
@@ -29,7 +29,7 @@ export default function ({ getService }: FtrProviderContext) {
return response.body;
};
const listResponse = await fetchPackageList();
- expect(listResponse.response.length).to.be(9);
+ expect(listResponse.response.length).not.to.be(0);
} else {
warnAndSkipTest(this, log);
}
diff --git a/x-pack/test/plugin_api_integration/test_suites/task_manager/task_manager_integration.js b/x-pack/test/plugin_api_integration/test_suites/task_manager/task_manager_integration.js
index c87a5039360b8..ea95eb42dd6ff 100644
--- a/x-pack/test/plugin_api_integration/test_suites/task_manager/task_manager_integration.js
+++ b/x-pack/test/plugin_api_integration/test_suites/task_manager/task_manager_integration.js
@@ -28,7 +28,8 @@ export default function ({ getService }) {
const testHistoryIndex = '.kibana_task_manager_test_result';
const supertest = supertestAsPromised(url.format(config.get('servers.kibana')));
- describe('scheduling and running tasks', () => {
+ // FLAKY: https://github.com/elastic/kibana/issues/71390
+ describe.skip('scheduling and running tasks', () => {
beforeEach(
async () => await supertest.delete('/api/sample_tasks').set('kbn-xsrf', 'xxx').expect(200)
);
diff --git a/x-pack/test/security_solution_cypress/es_archives/export_rule/mappings.json b/x-pack/test/security_solution_cypress/es_archives/export_rule/mappings.json
index dc92d23a618d3..249b03981386d 100644
--- a/x-pack/test/security_solution_cypress/es_archives/export_rule/mappings.json
+++ b/x-pack/test/security_solution_cypress/es_archives/export_rule/mappings.json
@@ -41,7 +41,7 @@
"infrastructure-ui-source": "2b2809653635caf490c93f090502d04c",
"ingest-agent-policies": "9326f99c977fd2ef5ab24b6336a0675c",
"ingest-outputs": "8aa988c376e65443fefc26f1075e93a3",
- "ingest-package-configs": "48e8bd97e488008e21c0b5a2367b83ad",
+ "ingest-package-policies": "48e8bd97e488008e21c0b5a2367b83ad",
"ingest_manager_settings": "012cf278ec84579495110bb827d1ed09",
"inventory-view": "88fc7e12fd1b45b6f0787323ce4f18d2",
"kql-telemetry": "d12a98a6f19a2d273696597547e064ee",
@@ -1286,7 +1286,7 @@
}
}
},
- "ingest-package-configs": {
+ "ingest-package-policies": {
"properties": {
"config_id": {
"type": "keyword"
@@ -1320,9 +1320,9 @@
"config": {
"type": "flattened"
},
- "dataset": {
+ "data_stream": {
"properties": {
- "name": {
+ "dataset": {
"type": "keyword"
},
"type": {
diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts
index d4947222a6cc0..02f893029f819 100644
--- a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts
+++ b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts
@@ -108,7 +108,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
inputs: [
{
id: policyInfo.packageConfig.id,
- dataset: { namespace: 'default' },
+ data_stream: { namespace: 'default' },
name: 'Protect East Coast',
meta: {
package: {
diff --git a/yarn.lock b/yarn.lock
index 7aff34fab23ce..f17418f07c5cc 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4306,11 +4306,6 @@
resolved "https://registry.yarnpkg.com/@types/boom/-/boom-7.2.0.tgz#19c36cbb5811a7493f0f2e37f31d42b28df1abc1"
integrity sha512-HonbGsHFbskh9zRAzA6tabcw18mCOsSEOL2ibGAuVqk6e7nElcRmWO5L4UfIHpDbWBWw+eZYFdsQ1+MEGgpcVA==
-"@types/browserslist-useragent@^3.0.0":
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/@types/browserslist-useragent/-/browserslist-useragent-3.0.0.tgz#d425c9818182ce71ce53866798cee9c7d41d6e53"
- integrity sha512-ZBvKzg3yyWNYEkwxAzdmUzp27sFvw+1m080/+2lwrt+eltNefn1f4fnpMyrjOla31p8zLleCYqQXw+3EETfn0w==
-
"@types/cacheable-request@^6.0.1":
version "6.0.1"
resolved "https://registry.yarnpkg.com/@types/cacheable-request/-/cacheable-request-6.0.1.tgz#5d22f3dded1fd3a84c0bbeb5039a7419c2c91976"
@@ -8601,15 +8596,6 @@ browserify-zlib@^0.2.0:
dependencies:
pako "~1.0.5"
-browserslist-useragent@^3.0.2:
- version "3.0.2"
- resolved "https://registry.yarnpkg.com/browserslist-useragent/-/browserslist-useragent-3.0.2.tgz#f0e209b2742baa5de0e451b52e678e8b4402617c"
- integrity sha512-/UPzK9xZnk5mwwWx4wcuBKAKx/mD3MNY8sUuZ2NPqnr4RVFWZogX+8mOP0cQEYo8j78sHk0hiDNaVXZ1U3hM9A==
- dependencies:
- browserslist "^4.6.6"
- semver "^6.3.0"
- useragent "^2.3.0"
-
browserslist@4.6.6:
version "4.6.6"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.6.6.tgz#6e4bf467cde520bc9dbdf3747dafa03531cec453"
@@ -8629,7 +8615,7 @@ browserslist@^4.12.0:
node-releases "^1.1.53"
pkg-up "^2.0.0"
-browserslist@^4.6.6, browserslist@^4.8.3:
+browserslist@^4.8.3:
version "4.8.5"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.8.5.tgz#691af4e327ac877b25e7a3f7ee869c4ef36cdea3"
integrity sha512-4LMHuicxkabIB+n9874jZX/az1IaZ5a+EUuvD7KFOu9x/Bd5YHyO0DIz2ls/Kl8g0ItS4X/ilEgf4T1Br0lgSg==
@@ -29564,13 +29550,6 @@ tmp@0.0.30:
dependencies:
os-tmpdir "~1.0.1"
-tmp@0.0.x, tmp@^0.0.33:
- version "0.0.33"
- resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
- integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==
- dependencies:
- os-tmpdir "~1.0.2"
-
tmp@0.1.0, tmp@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.1.0.tgz#ee434a4e22543082e294ba6201dcc6eafefa2877"
@@ -29585,6 +29564,13 @@ tmp@^0.0.29:
dependencies:
os-tmpdir "~1.0.1"
+tmp@^0.0.33:
+ version "0.0.33"
+ resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
+ integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==
+ dependencies:
+ os-tmpdir "~1.0.2"
+
tmpl@1.0.x:
version "1.0.4"
resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1"
@@ -30711,14 +30697,6 @@ user-home@^2.0.0:
dependencies:
os-homedir "^1.0.0"
-useragent@^2.3.0:
- version "2.3.0"
- resolved "https://registry.yarnpkg.com/useragent/-/useragent-2.3.0.tgz#217f943ad540cb2128658ab23fc960f6a88c9972"
- integrity sha512-4AoH4pxuSvHCjqLO04sU6U/uE65BYza8l/KKBS0b0hnUPWi+cQ2BpeTEwejCSx9SPV5/U03nniDTrWx5NrmKdw==
- dependencies:
- lru-cache "4.1.x"
- tmp "0.0.x"
-
utif@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/utif/-/utif-2.0.1.tgz#9e1582d9bbd20011a6588548ed3266298e711759"