[Osquery] Response Actions fixes (#141041)

This commit is contained in:
Tomasz Ciecierski 2022-09-23 21:13:26 +02:00 committed by GitHub
parent d45ab6dbaa
commit e50db36c16
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
52 changed files with 517 additions and 199 deletions

1
.github/CODEOWNERS vendored
View file

@ -894,6 +894,7 @@ packages/kbn-mapbox-gl @elastic/kibana-gis
packages/kbn-monaco @elastic/kibana-app-services
packages/kbn-optimizer @elastic/kibana-operations
packages/kbn-optimizer-webpack-helpers @elastic/kibana-operations
packages/kbn-osquery-io-ts-types @elastic/security-asset-management
packages/kbn-performance-testing-dataset-extractor @elastic/kibana-performance-testing
packages/kbn-plugin-discovery @elastic/kibana-operations
packages/kbn-plugin-generator @elastic/kibana-operations

View file

@ -325,6 +325,7 @@
"@kbn/ml-is-populated-object": "link:bazel-bin/x-pack/packages/ml/is_populated_object",
"@kbn/ml-string-hash": "link:bazel-bin/x-pack/packages/ml/string_hash",
"@kbn/monaco": "link:bazel-bin/packages/kbn-monaco",
"@kbn/osquery-io-ts-types": "link:bazel-bin/packages/kbn-osquery-io-ts-types",
"@kbn/plugin-discovery": "link:bazel-bin/packages/kbn-plugin-discovery",
"@kbn/react-field": "link:bazel-bin/packages/kbn-react-field",
"@kbn/rule-data-utils": "link:bazel-bin/packages/kbn-rule-data-utils",
@ -1068,6 +1069,7 @@
"@types/kbn__monaco": "link:bazel-bin/packages/kbn-monaco/npm_module_types",
"@types/kbn__optimizer": "link:bazel-bin/packages/kbn-optimizer/npm_module_types",
"@types/kbn__optimizer-webpack-helpers": "link:bazel-bin/packages/kbn-optimizer-webpack-helpers/npm_module_types",
"@types/kbn__osquery-io-ts-types": "link:bazel-bin/packages/kbn-osquery-io-ts-types/npm_module_types",
"@types/kbn__performance-testing-dataset-extractor": "link:bazel-bin/packages/kbn-performance-testing-dataset-extractor/npm_module_types",
"@types/kbn__plugin-discovery": "link:bazel-bin/packages/kbn-plugin-discovery/npm_module_types",
"@types/kbn__plugin-generator": "link:bazel-bin/packages/kbn-plugin-generator/npm_module_types",

View file

@ -236,6 +236,7 @@ filegroup(
"//packages/kbn-monaco:build",
"//packages/kbn-optimizer:build",
"//packages/kbn-optimizer-webpack-helpers:build",
"//packages/kbn-osquery-io-ts-types:build",
"//packages/kbn-performance-testing-dataset-extractor:build",
"//packages/kbn-plugin-discovery:build",
"//packages/kbn-plugin-generator:build",
@ -555,6 +556,7 @@ filegroup(
"//packages/kbn-monaco:build_types",
"//packages/kbn-optimizer:build_types",
"//packages/kbn-optimizer-webpack-helpers:build_types",
"//packages/kbn-osquery-io-ts-types:build_types",
"//packages/kbn-performance-testing-dataset-extractor:build_types",
"//packages/kbn-plugin-discovery:build_types",
"//packages/kbn-plugin-generator:build_types",

View file

@ -0,0 +1,127 @@
load("@npm//@bazel/typescript:index.bzl", "ts_config")
load("@build_bazel_rules_nodejs//:index.bzl", "js_library")
load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", "ts_project")
PKG_DIRNAME = "kbn-osquery-io-ts-types"
PKG_REQUIRE_NAME = "@kbn/osquery-io-ts-types"
SOURCE_FILES = glob(
[
"**/*.ts",
],
exclude = [
"**/*.config.js",
"**/*.mock.*",
"**/*.test.*",
"**/*.stories.*",
"**/__snapshots__/**",
"**/integration_tests/**",
"**/mocks/**",
"**/scripts/**",
"**/storybook/**",
"**/test_fixtures/**",
"**/test_helpers/**",
],
)
SRCS = SOURCE_FILES
filegroup(
name = "srcs",
srcs = SRCS,
)
NPM_MODULE_EXTRA_FILES = [
"package.json",
]
# In this array place runtime dependencies, including other packages and NPM packages
# which must be available for this code to run.
#
# To reference other packages use:
# "//repo/relative/path/to/package"
# eg. "//packages/kbn-utils"
#
# To reference a NPM package use:
# "@npm//name-of-package"
# eg. "@npm//lodash"
RUNTIME_DEPS = [
"@npm//io-ts",
]
# In this array place dependencies necessary to build the types, which will include the
# :npm_module_types target of other packages and packages from NPM, including @types/*
# packages.
#
# To reference the types for another package use:
# "//repo/relative/path/to/package:npm_module_types"
# eg. "//packages/kbn-utils:npm_module_types"
#
# References to NPM packages work the same as RUNTIME_DEPS
TYPES_DEPS = [
"@npm//@types/node",
"@npm//@types/jest",
"@npm//tslib",
"@npm//io-ts",
]
jsts_transpiler(
name = "target_node",
srcs = SRCS,
build_pkg_name = package_name(),
)
ts_config(
name = "tsconfig",
src = "tsconfig.json",
deps = [
"//:tsconfig.base.json",
"//:tsconfig.bazel.json",
],
)
ts_project(
name = "tsc_types",
args = ['--pretty'],
srcs = SRCS,
deps = TYPES_DEPS,
declaration = True,
declaration_map = True,
emit_declaration_only = True,
out_dir = "target_types",
tsconfig = ":tsconfig",
)
js_library(
name = PKG_DIRNAME,
srcs = NPM_MODULE_EXTRA_FILES,
deps = RUNTIME_DEPS + [":target_node"],
package_name = PKG_REQUIRE_NAME,
visibility = ["//visibility:public"],
)
pkg_npm(
name = "npm_module",
deps = [":" + PKG_DIRNAME],
)
filegroup(
name = "build",
srcs = [":npm_module"],
visibility = ["//visibility:public"],
)
pkg_npm_types(
name = "npm_module_types",
srcs = SRCS,
deps = [":tsc_types"],
package_name = PKG_REQUIRE_NAME,
tsconfig = ":tsconfig",
visibility = ["//visibility:public"],
)
filegroup(
name = "build_types",
srcs = [":npm_module_types"],
visibility = ["//visibility:public"],
)

View file

@ -0,0 +1,3 @@
# kbn-osquery-io-ts-types
Types that are specific to the osquery plugin to be shared among plugins.

View file

@ -0,0 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export * from './src/live_query';

View file

@ -0,0 +1,13 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
module.exports = {
preset: '@kbn/test/jest_node',
rootDir: '../..',
roots: ['<rootDir>/packages/kbn-osquery-io-ts-types'],
};

View file

@ -0,0 +1,7 @@
{
"type": "shared-common",
"id": "@kbn/osquery-io-ts-types",
"owner": "@elastic/security-asset-management",
"runtimeDeps": [],
"typeDeps": [],
}

View file

@ -0,0 +1,9 @@
{
"name": "@kbn/osquery-io-ts-types",
"private": true,
"version": "1.0.0",
"description": "io ts utilities and types to be shared with plugins from the osquery project",
"main": "./target_node/index.js",
"browser": "./target_web/index.js",
"license": "SSPL-1.0 OR Elastic License 2.0"
}

View file

@ -1,8 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import * as t from 'io-ts';
@ -95,9 +96,9 @@ export const arrayQueries = t.array(
t.type({
id,
query,
ecs_mapping: ecsMapping,
version,
platform,
ecs_mapping: ecsMappingOrUndefined,
version: versionOrUndefined,
platform: platformOrUndefined,
})
);
export type ArrayQueries = t.TypeOf<typeof arrayQueries>;

View file

@ -0,0 +1,16 @@
{
"extends": "../../tsconfig.bazel.json",
"compilerOptions": {
"declaration": true,
"declarationMap": true,
"emitDeclarationOnly": true,
"outDir": "target_types",
"types": [
"jest",
"node"
]
},
"include": [
"**/*.ts",
]
}

View file

@ -5,5 +5,4 @@
* 2.0.
*/
export * from './schemas';
export * from './utils';

View file

@ -7,9 +7,7 @@
import { isEmpty, reduce } from 'lodash';
import type { DefaultValues } from 'react-hook-form';
import type { ECSMapping } from './schemas';
export type { ECSMapping };
import type { ECSMapping } from '@kbn/osquery-io-ts-types';
export type ECSMappingArray = Array<{
key: string;

View file

@ -13,7 +13,7 @@ import {
packIdOrUndefined,
queryOrUndefined,
queriesOrUndefined,
} from '../../common/schemas';
} from '@kbn/osquery-io-ts-types';
export const createLiveQueryRequestBodySchema = t.partial({
agent_ids: t.array(t.string),

View file

@ -7,7 +7,7 @@
import * as t from 'io-ts';
import type { Description } from '../../common/schemas';
import type { Description } from '@kbn/osquery-io-ts-types';
import {
id,
descriptionOrUndefined,
@ -18,7 +18,7 @@ import {
snapshotOrUndefined,
removedOrUndefined,
ecsMappingOrUndefined,
} from '../../common/schemas';
} from '@kbn/osquery-io-ts-types';
import type { RequiredKeepUndefined } from '../../../types';
export const createSavedQueryRequestSchema = t.type({

View file

@ -9,7 +9,7 @@ import { useQuery } from '@tanstack/react-query';
import { i18n } from '@kbn/i18n';
import { filter } from 'lodash';
import type { ECSMapping } from '../../common/schemas/common';
import type { ECSMapping } from '@kbn/osquery-io-ts-types';
import { useKibana } from '../common/lib/kibana';
import type { ESTermQuery } from '../../common/typed_json';
import { useErrorToast } from '../common/hooks/use_error_toast';

View file

@ -7,6 +7,7 @@
import { EuiButton, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import type { ECSMapping } from '@kbn/osquery-io-ts-types';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useForm as useHookForm, FormProvider } from 'react-hook-form';
import { isEmpty, find, pickBy } from 'lodash';
@ -14,7 +15,6 @@ import { isEmpty, find, pickBy } from 'lodash';
import type { AddToTimelinePayload } from '../../timelines/get_add_to_timeline';
import { QueryPackSelectable } from './query_pack_selectable';
import type { SavedQuerySOFormData } from '../../saved_queries/form/use_saved_query_form';
import type { ECSMapping } from '../../../common/schemas/common/utils';
import { useKibana } from '../../common/lib/kibana';
import { ResultTabs } from '../../routes/saved_queries/edit/tabs';
import { SavedQueryFlyout } from '../../saved_queries';

View file

@ -97,6 +97,15 @@ const LiveQueryQueryFieldComponent: React.FC<LiveQueryQueryFieldProps> = ({
[permissions.writeLiveQueries]
);
const isAdvancedToggleHidden = useMemo(
() =>
!(
permissions.writeLiveQueries ||
permissions.runSavedQueries ||
permissions.readSavedQueries
),
[permissions.readSavedQueries, permissions.runSavedQueries, permissions.writeLiveQueries]
);
const isSavedQueryDisabled = useMemo(
() => !permissions.runSavedQueries || !permissions.readSavedQueries,
[permissions.readSavedQueries, permissions.runSavedQueries]
@ -143,15 +152,17 @@ const LiveQueryQueryFieldComponent: React.FC<LiveQueryQueryFieldProps> = ({
<EuiSpacer size="m" />
<StyledEuiAccordion
id="advanced"
forceState={advancedContentState}
onToggle={handleToggle}
buttonContent="Advanced"
>
<EuiSpacer size="xs" />
<ECSMappingEditorField euiFieldProps={ecsFieldProps} />
</StyledEuiAccordion>
{!isAdvancedToggleHidden && (
<StyledEuiAccordion
id="advanced"
forceState={advancedContentState}
onToggle={handleToggle}
buttonContent="Advanced"
>
<EuiSpacer size="xs" />
<ECSMappingEditorField euiFieldProps={ecsFieldProps} />
</StyledEuiAccordion>
)}
</>
);
};

View file

@ -34,7 +34,7 @@ import type {
import { DOCUMENT_FIELD_NAME as RECORDS_FIELD } from '@kbn/lens-plugin/common/constants';
import { FilterStateStore } from '@kbn/es-query';
import styled from 'styled-components';
import type { ECSMapping } from '../../../common/schemas/common';
import type { ECSMapping } from '@kbn/osquery-io-ts-types';
import { SECURITY_APP_NAME } from '../../timelines/get_add_to_timeline';
import type { AddToTimelinePayload } from '../../timelines/get_add_to_timeline';
import { PackResultsHeader } from './pack_results_header';

View file

@ -10,7 +10,7 @@ import { EuiCode, EuiLoadingContent, EuiEmptyPrompt } from '@elastic/eui';
import React, { useMemo } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import type { ECSMapping } from '../../common/schemas/common';
import type { ECSMapping } from '@kbn/osquery-io-ts-types';
import type { AddToTimelinePayload } from '../timelines/get_add_to_timeline';
import { LiveQueryForm } from './form';
import { useActionResultsPrivileges } from '../action_results/use_action_privileges';

View file

@ -6,7 +6,7 @@
*/
import { useMutation } from '@tanstack/react-query';
import type { AgentSelection } from '../../common/schemas/common';
import type { AgentSelection } from '@kbn/osquery-io-ts-types';
import type { CreateLiveQueryRequestBodySchema } from '../../common/schemas/routes/live_query';
import { useKibana } from '../common/lib/kibana';
import { useErrorToast } from '../common/hooks/use_error_toast';

View file

@ -42,7 +42,9 @@ import deepEqual from 'fast-deep-equal';
import type { InternalFieldErrors, UseFieldArrayRemove, UseFormReturn } from 'react-hook-form';
import { useForm, useController, useFieldArray, useFormContext } from 'react-hook-form';
import type { ECSMappingArray, ECSMapping } from '../../../common/schemas/common/utils';
import type { ECSMapping } from '@kbn/osquery-io-ts-types';
import type { ECSMappingArray } from '../../../common/schemas/common/utils';
import {
convertECSMappingToArray,
convertECSMappingToObject,

View file

@ -10,7 +10,7 @@ import { useForm as useHookForm } from 'react-hook-form';
import type { Draft } from 'immer';
import { produce } from 'immer';
import { useMemo } from 'react';
import type { ECSMapping } from '../../../common/schemas/common';
import type { ECSMapping } from '@kbn/osquery-io-ts-types';
export interface UsePackQueryFormProps {
uniqueQueryIds: string[];

View file

@ -26,10 +26,9 @@ import {
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import React, { createContext, useEffect, useState, useCallback, useContext, useMemo } from 'react';
import type { ECSMapping } from '@kbn/osquery-io-ts-types';
import { pagePathGetters } from '@kbn/fleet-plugin/public';
import type { AddToTimelinePayload } from '../timelines/get_add_to_timeline';
import type { ECSMapping } from '../../common/schemas/common';
import { useAllResults } from './use_all_results';
import type { ResultEdges } from '../../common/search_strategy';
import { Direction } from '../../common/search_strategy';

View file

@ -8,10 +8,10 @@
import { EuiTabbedContent, EuiNotificationBadge } from '@elastic/eui';
import React, { useMemo } from 'react';
import type { ReactElement } from 'react';
import { useKibana } from '../../../common/lib/kibana';
import type { ECSMapping } from '@kbn/osquery-io-ts-types';
import { useKibana } from '../../../common/lib/kibana';
import type { AddToTimelinePayload } from '../../../timelines/get_add_to_timeline';
import type { ECSMapping } from '../../../../common/schemas/common';
import { ResultsTable } from '../../../results/results_table';
import { ActionResultsSummary } from '../../../action_results/action_results_summary';

View file

@ -23,8 +23,8 @@ import { useHistory } from 'react-router-dom';
import deepEqual from 'fast-deep-equal';
import type { SavedObject } from '@kbn/core/public';
import type { ECSMapping } from '@kbn/osquery-io-ts-types';
import { Direction } from '../../../../common/search_strategy';
import type { ECSMapping } from '../../../../common/schemas/common';
import { WithHeaderLayout } from '../../../components/layouts';
import { useBreadcrumbs } from '../../../common/hooks/use_breadcrumbs';
import { useKibana, useRouterNavigate } from '../../../common/lib/kibana';

View file

@ -10,7 +10,7 @@ import { isArray, isEmpty, map } from 'lodash';
import type { Draft } from 'immer';
import produce from 'immer';
import { useMemo } from 'react';
import type { ECSMapping } from '../../../common/schemas/common';
import type { ECSMapping } from '@kbn/osquery-io-ts-types';
import { useSavedQueries } from '../use_saved_queries';
export interface SavedQuerySOFormData {

View file

@ -12,7 +12,7 @@ import { useForm as useHookForm, FormProvider } from 'react-hook-form';
import { get, isEmpty, map } from 'lodash';
import useEffectOnce from 'react-use/lib/useEffectOnce';
import type { ECSMapping } from '../../../common/schemas/common/utils';
import type { ECSMapping } from '@kbn/osquery-io-ts-types';
import { QueryPackSelectable } from '../../live_queries/form/query_pack_selectable';
import { useFormContext } from '../../shared_imports';
import type { ArrayItem } from '../../shared_imports';
@ -98,7 +98,7 @@ const OsqueryResponseActionParamsFormComponent = forwardRef<
id: watchedValues.id,
savedQueryId: watchedValues.savedQueryId,
query: watchedValues.query,
ecs_mapping: watchedValues.ecs_mapping,
ecsMapping: watchedValues.ecs_mapping,
},
},
});
@ -139,13 +139,18 @@ const OsqueryResponseActionParamsFormComponent = forwardRef<
useEffectOnce(() => {
if (defaultParams && defaultParams.id) {
const { packId, ...restParams } = defaultParams;
const { packId, ecsMapping, ...restParams } = defaultParams;
// TODO change map into forEach, and type defaultParams
map(restParams, (value, key: keyof OsqueryResponseActionsParamsFormFields) => {
if (!isEmpty(value)) {
setValue(key, value);
}
});
if (!isEmpty(ecsMapping)) {
setValue('ecs_mapping', ecsMapping);
}
if (!isEmpty(packId)) {
setValue('packId', [packId]);
}

View file

@ -10,7 +10,7 @@ import type { ReactElement } from 'react';
import React, { useMemo } from 'react';
import { find } from 'lodash';
import { useWatch } from 'react-hook-form';
import type { ECSMapping } from '../../../common/schemas/common';
import type { ECSMapping } from '@kbn/osquery-io-ts-types';
import { PackQueriesStatusTable } from '../../live_queries/form/pack_queries_status_table';
import { usePacks } from '../../packs/use_packs';
import { PacksComboBoxField } from '../../live_queries/form/packs_combobox_field';

View file

@ -209,12 +209,14 @@ export class TelemetryEventsSender {
type: 'boolean',
_meta: {
description: '',
optional: true,
},
},
prebuilt: {
type: 'boolean',
_meta: {
description: '',
optional: true,
},
},
ecs_mapping: {

View file

@ -6,28 +6,22 @@
*/
import * as t from 'io-ts';
import { ecsMapping, arrayQueries } from '@kbn/osquery-io-ts-types';
export const OsqueryParams = t.intersection([
t.type({
id: t.string,
}),
t.partial({
query: t.union([t.string, t.undefined]),
ecs_mapping: t.record(t.string, t.record(t.string, t.any)),
queries: t.array(
t.intersection([
t.type({
id: t.string,
query: t.string,
}),
t.partial({
ecs_mapping: t.record(t.string, t.record(t.string, t.any)),
platform: t.union([t.string, t.undefined]),
interval: t.union([t.number, t.undefined]),
}),
])
),
packId: t.union([t.string, t.undefined]),
savedQueryId: t.union([t.string, t.undefined]),
}),
]);
export const OsqueryParams = t.type({
id: t.string,
query: t.union([t.string, t.undefined]),
ecs_mapping: t.union([ecsMapping, t.undefined]),
queries: t.union([arrayQueries, t.undefined]),
pack_id: t.union([t.string, t.undefined]),
saved_query_id: t.union([t.string, t.undefined]),
});
export const OsqueryParamsCamelCase = t.type({
id: t.string,
query: t.union([t.string, t.undefined]),
ecsMapping: t.union([ecsMapping, t.undefined]),
queries: t.union([arrayQueries, t.undefined]),
packId: t.union([t.string, t.undefined]),
savedQueryId: t.union([t.string, t.undefined]),
});

View file

@ -6,7 +6,7 @@
*/
import * as t from 'io-ts';
import { OsqueryParams } from './osquery';
import { OsqueryParams, OsqueryParamsCamelCase } from './osquery';
export enum RESPONSE_ACTION_TYPES {
OSQUERY = '.osquery',
@ -18,7 +18,7 @@ export const SUPPORTED_RESPONSE_ACTION_TYPES = Object.values(RESPONSE_ACTION_TYP
const ResponseActionRuleParam = t.exact(
t.type({
actionTypeId: t.literal(RESPONSE_ACTION_TYPES.OSQUERY),
params: OsqueryParams,
params: OsqueryParamsCamelCase,
})
);
export type RuleResponseAction = t.TypeOf<typeof ResponseActionRuleParam>;

View file

@ -50,12 +50,24 @@ describe('transform_actions', () => {
action_type_id: RESPONSE_ACTION_TYPES.OSQUERY,
params: {
id: 'test',
ecs_mapping: {},
saved_query_id: undefined,
pack_id: undefined,
query: undefined,
queries: undefined,
},
};
const alertAction = transformRuleToAlertResponseAction(ruleAction);
expect(alertAction).toEqual({
actionTypeId: ruleAction.action_type_id,
params: ruleAction.params,
params: {
id: 'test',
ecsMapping: {},
savedQueryId: undefined,
packId: undefined,
query: undefined,
queries: undefined,
},
});
});
@ -64,12 +76,24 @@ describe('transform_actions', () => {
actionTypeId: RESPONSE_ACTION_TYPES.OSQUERY,
params: {
id: 'test',
ecsMapping: {},
savedQueryId: undefined,
packId: undefined,
query: undefined,
queries: undefined,
},
};
const ruleAction = transformAlertToRuleResponseAction(alertAction);
expect(ruleAction).toEqual({
action_type_id: alertAction.actionTypeId,
params: alertAction.params,
params: {
id: 'test',
ecs_mapping: {},
saved_query_id: undefined,
pack_id: undefined,
query: undefined,
queries: undefined,
},
});
});
});

View file

@ -37,8 +37,20 @@ export const transformRuleToAlertResponseAction = ({
action_type_id: actionTypeId,
params,
}: ResponseAction): RuleResponseAction => {
const {
saved_query_id: savedQueryId,
ecs_mapping: ecsMapping,
pack_id: packId,
...rest
} = params;
return {
params,
params: {
...rest,
savedQueryId,
ecsMapping,
packId,
},
actionTypeId,
};
};
@ -47,8 +59,14 @@ export const transformAlertToRuleResponseAction = ({
actionTypeId,
params,
}: RuleResponseAction): ResponseAction => {
const { savedQueryId, ecsMapping, packId, ...rest } = params;
return {
params,
params: {
...rest,
saved_query_id: savedQueryId,
ecs_mapping: ecsMapping,
pack_id: packId,
},
action_type_id: actionTypeId,
};
};

View file

@ -20,6 +20,7 @@ import React, { useCallback, useMemo, useState } from 'react';
import styled from 'styled-components';
import { isEmpty } from 'lodash';
import type { AlertRawEventData } from './osquery_tab';
import { useOsqueryTab } from './osquery_tab';
import { EventFieldsBrowser } from './event_fields_browser';
import { JsonView } from './json_view';
@ -51,21 +52,6 @@ export const EVENT_DETAILS_CONTEXT_ID = 'event-details';
type EventViewTab = EuiTabbedContentTab;
export interface AlertRawEventData {
_id: string;
fields: {
['agent.id']?: string[];
['kibana.alert.rule.parameters']: Array<{
response_actions: Array<{
action_type_id: string;
params: Record<string, unknown>;
}>;
}>;
['kibana.alert.rule.name']: string[];
};
[key: string]: unknown;
}
export type EventViewId =
| EventsViewType.tableView
| EventsViewType.jsonView
@ -130,6 +116,7 @@ const TabContentWrapper = styled.div`
const RendererContainer = styled.div`
overflow-x: auto;
padding-right: ${(props) => props.theme.eui.euiSizeXS};
& .${DETAILS_CLASS_NAME} .euiFlexGroup {
justify-content: flex-start;
}
@ -397,7 +384,6 @@ const EventDetailsComponent: React.FC<Props> = ({
const osqueryTab = useOsqueryTab({
rawEventData: rawEventData as AlertRawEventData,
id,
});
const tabs = useMemo(() => {

View file

@ -5,13 +5,21 @@
* 2.0.
*/
import { EuiFlexGroup, EuiFlexItem, EuiNotificationBadge } from '@elastic/eui';
import React from 'react';
import {
EuiCode,
EuiEmptyPrompt,
EuiFlexGroup,
EuiFlexItem,
EuiNotificationBadge,
} from '@elastic/eui';
import React, { useMemo } from 'react';
import styled from 'styled-components';
import { FormattedMessage } from '@kbn/i18n-react';
import { PERMISSION_DENIED } from '../../../detection_engine/rule_response_actions/osquery/translations';
import { expandDottedObject } from '../../../../common/utils/expand_dotted';
import { RESPONSE_ACTION_TYPES } from '../../../../common/detection_engine/rule_response_actions/schemas';
import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features';
import { useKibana } from '../../lib/kibana';
import type { AlertRawEventData } from './event_details';
import { EventsViewType } from './event_details';
import * as i18n from './translations';
import { useHandleAddToTimeline } from './add_to_timeline_button';
@ -20,68 +28,126 @@ const TabContentWrapper = styled.div`
height: 100%;
position: relative;
`;
type RuleParameters = Array<{
response_actions: Array<{
action_type_id: string;
params: Record<string, unknown>;
}>;
}>;
export const useOsqueryTab = ({
rawEventData,
id,
}: {
rawEventData?: AlertRawEventData;
id: string;
}) => {
export interface AlertRawEventData {
_id: string;
fields: {
['agent.id']?: string[];
['kibana.alert.rule.parameters']: RuleParameters;
['kibana.alert.rule.name']: string[];
};
[key: string]: unknown;
}
interface ExpandedEventFieldsObject {
agent?: {
id: string[];
};
kibana: {
alert?: {
rule?: {
parameters: RuleParameters;
name: string[];
};
};
};
}
export const useOsqueryTab = ({ rawEventData }: { rawEventData?: AlertRawEventData }) => {
const {
services: { osquery },
services: { osquery, application },
} = useKibana();
const handleAddToTimeline = useHandleAddToTimeline();
const responseActionsEnabled = useIsExperimentalFeatureEnabled('responseActionsEnabled');
const emptyPrompt = useMemo(
() => (
<EuiEmptyPrompt
iconType="logoOsquery"
title={<h2>{PERMISSION_DENIED}</h2>}
titleSize="xs"
body={
<FormattedMessage
id="xpack.securitySolution.osquery.results.missingPrivilleges"
defaultMessage="To access these results, ask your administrator for {osquery} Kibana
privileges."
values={{
// eslint-disable-next-line react/jsx-no-literals
osquery: <EuiCode>osquery</EuiCode>,
}}
/>
}
/>
),
[]
);
if (!osquery || !rawEventData || !responseActionsEnabled) {
return;
}
const { OsqueryResults } = osquery;
const parameters = rawEventData.fields['kibana.alert.rule.parameters'];
const expandedEventFieldsObject = expandDottedObject(
rawEventData.fields
) as ExpandedEventFieldsObject;
const parameters = expandedEventFieldsObject.kibana?.alert?.rule?.parameters;
const responseActions = parameters?.[0].response_actions;
const osqueryActionsLength = responseActions?.filter(
(action: { action_type_id: string }) => action.action_type_id === RESPONSE_ACTION_TYPES.OSQUERY
)?.length;
const agentIds = rawEventData.fields['agent.id'];
const ruleName = rawEventData.fields['kibana.alert.rule.name'];
if (!osqueryActionsLength) {
return;
}
const ruleName = expandedEventFieldsObject.kibana?.alert?.rule?.name;
const agentIds = expandedEventFieldsObject.agent?.id;
const alertId = rawEventData._id;
return osqueryActionsLength
? {
id: EventsViewType.osqueryView,
'data-test-subj': 'osqueryViewTab',
name: (
<EuiFlexGroup
direction="row"
alignItems={'center'}
justifyContent={'spaceAround'}
gutterSize="xs"
>
<EuiFlexItem>
<span>{i18n.OSQUERY_VIEW}</span>
</EuiFlexItem>
<EuiFlexItem>
<EuiNotificationBadge data-test-subj="osquery-actions-notification">
{osqueryActionsLength}
</EuiNotificationBadge>
</EuiFlexItem>
</EuiFlexGroup>
),
content: (
<>
<TabContentWrapper data-test-subj="osqueryViewWrapper">
<OsqueryResults
agentIds={agentIds}
ruleName={ruleName}
alertId={alertId}
addToTimeline={handleAddToTimeline}
/>
</TabContentWrapper>
</>
),
}
: undefined;
return {
id: EventsViewType.osqueryView,
'data-test-subj': 'osqueryViewTab',
name: (
<EuiFlexGroup
direction="row"
alignItems={'center'}
justifyContent={'spaceAround'}
gutterSize="xs"
>
<EuiFlexItem>
<span>{i18n.OSQUERY_VIEW}</span>
</EuiFlexItem>
<EuiFlexItem>
<EuiNotificationBadge data-test-subj="osquery-actions-notification">
{osqueryActionsLength}
</EuiNotificationBadge>
</EuiFlexItem>
</EuiFlexGroup>
),
content: (
<>
<TabContentWrapper data-test-subj="osqueryViewWrapper">
{!application?.capabilities?.osquery?.read ? (
emptyPrompt
) : (
<OsqueryResults
agentIds={agentIds}
ruleName={ruleName}
alertId={alertId}
addToTimeline={handleAddToTimeline}
/>
)}
</TabContentWrapper>
</>
),
};
};

View file

@ -19,7 +19,7 @@ interface IProps {
}
export const OsqueryResponseAction = React.memo((props: IProps) => {
const { osquery } = useKibana().services;
const { osquery, application } = useKibana().services;
const OsqueryForm = useMemo(
() => osquery?.OsqueryResponseActionTypeForm,
[osquery?.OsqueryResponseActionTypeForm]
@ -27,13 +27,20 @@ export const OsqueryResponseAction = React.memo((props: IProps) => {
if (osquery) {
const { disabled, permissionDenied } = osquery?.fetchInstallationStatus();
const disabledOsqueryPermission = !(
application?.capabilities?.osquery?.writeLiveQueries ||
(application?.capabilities?.osquery?.runSavedQueries &&
(application?.capabilities?.osquery?.readSavedQueries ||
application?.capabilities?.osquery?.readPacks))
);
if (permissionDenied) {
if (permissionDenied || disabledOsqueryPermission) {
return (
<>
<EuiEmptyPrompt
title={<h2>{PERMISSION_DENIED}</h2>}
titleSize="xs"
iconType="logoOsquery"
body={
<p>
<FormattedMessage
@ -55,6 +62,7 @@ export const OsqueryResponseAction = React.memo((props: IProps) => {
if (disabled) {
return (
<EuiEmptyPrompt
iconType="logoOsquery"
title={<h2>{SHORT_EMPTY_TITLE}</h2>}
titleSize="xs"
body={<p>{NOT_AVAILABLE}</p>}

View file

@ -125,6 +125,7 @@ export const RuleActionsField: React.FC<Props> = ({ field, messageVariables }) =
setActionParamsProperty,
featureId: SecurityConnectorFeatureId,
defaultActionMessage: DEFAULT_ACTION_MESSAGE,
hideActionHeader: true,
}),
[
actions,

View file

@ -14,7 +14,7 @@ import { HeaderSection } from '../../../../common/components/header_section';
interface StepPanelProps {
children: React.ReactNode;
loading: boolean;
title: string;
title?: string;
}
const MyPanel = styled(EuiPanel)`
@ -33,7 +33,7 @@ const StepPanelComponent: React.FC<StepPanelProps> = ({ children, loading, title
data-test-subj="stepPanelProgress"
/>
)}
<HeaderSection title={title} />
{title && <HeaderSection title={title} />}
{children}
</MyPanel>
);

View file

@ -34,12 +34,5 @@ export const getSchema = ({
defaultMessage: 'Actions frequency',
}
),
helpText: i18n.translate(
'xpack.securitySolution.detectionEngine.createRule.stepRuleActions.fieldThrottleHelpText',
{
defaultMessage:
'Select when automated actions should be performed if a rule evaluates as true.',
}
),
},
});

View file

@ -13,10 +13,12 @@ import {
EuiButton,
EuiSpacer,
EuiText,
EuiTitle,
} from '@elastic/eui';
import { findIndex } from 'lodash/fp';
import type { FC } from 'react';
import React, { memo, useCallback, useEffect, useMemo, useRef } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import type { ActionVariables } from '@kbn/triggers-actions-ui-plugin/public';
import { UseArray } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
@ -40,6 +42,7 @@ import { getSchema } from './get_schema';
import * as I18n from './translations';
import { APP_UI_ID } from '../../../../../common/constants';
import { useManageCaseAction } from './use_manage_case_action';
import { THROTTLE_FIELD_HELP_TEXT, THROTTLE_FIELD_HELP_TEXT_WHEN_QUERY } from './translations';
interface StepRuleActionsProps extends RuleStepProps {
defaultValues?: ActionsStepRule | null;
@ -66,6 +69,22 @@ const getThrottleOptions = (throttle?: string | null) => {
return THROTTLE_OPTIONS;
};
const DisplayActionsHeader = () => {
return (
<>
<EuiTitle size="s">
<h4>
<FormattedMessage
defaultMessage="Actions"
id="xpack.securitySolution.detectionEngine.rule.editRule.actionSectionsTitle"
/>
</h4>
</EuiTitle>
<EuiSpacer size="l" />
</>
);
};
const StepRuleActionsComponent: FC<StepRuleActionsProps> = ({
addPadding = false,
defaultValues,
@ -164,11 +183,14 @@ const StepRuleActionsComponent: FC<StepRuleActionsProps> = ({
isLoading: isLoadingCaseAction,
dataTestSubj: 'detectionEngineStepRuleActionsThrottle',
hasNoInitialSelection: false,
helpText: isQueryRule(ruleType)
? THROTTLE_FIELD_HELP_TEXT_WHEN_QUERY
: THROTTLE_FIELD_HELP_TEXT,
euiFieldProps: {
options: throttleOptions,
},
}),
[isLoading, isLoadingCaseAction, throttleOptions]
[isLoading, isLoadingCaseAction, ruleType, throttleOptions]
);
const displayActionsOptions = useMemo(
@ -192,11 +214,9 @@ const StepRuleActionsComponent: FC<StepRuleActionsProps> = ({
const displayResponseActionsOptions = useMemo(() => {
if (isQueryRule(ruleType)) {
return (
<>
<UseArray path="responseActions">
{(params) => <ResponseActionsForm {...params} saveClickRef={saveClickRef} />}
</UseArray>
</>
<UseArray path="responseActions" initialNumberOfItems={0}>
{(params) => <ResponseActionsForm {...params} saveClickRef={saveClickRef} />}
</UseArray>
);
}
return null;
@ -205,6 +225,7 @@ const StepRuleActionsComponent: FC<StepRuleActionsProps> = ({
const displayActionsDropDown = useMemo(() => {
return application.capabilities.actions.show ? (
<>
<DisplayActionsHeader />
<UseField
path="throttle"
component={ThrottleSelectField}

View file

@ -28,3 +28,19 @@ export const NO_ACTIONS_READ_PERMISSIONS = i18n.translate(
'Cannot create rule actions. You do not have "Read" permissions for the "Actions" plugin.',
}
);
export const THROTTLE_FIELD_HELP_TEXT = i18n.translate(
'xpack.securitySolution.detectionEngine.createRule.stepRuleActions.fieldThrottleHelpText',
{
defaultMessage:
'Select when automated actions should be performed if a rule evaluates as true.',
}
);
export const THROTTLE_FIELD_HELP_TEXT_WHEN_QUERY = i18n.translate(
'xpack.securitySolution.detectionEngine.createRule.stepRuleActions.fieldThrottleHelpTextWhenQuery',
{
defaultMessage:
'Select when automated actions should be performed if a rule evaluates as true. This frequency does not apply to Response Actions.',
}
);

View file

@ -281,7 +281,7 @@ const EditRulePageComponent: FC = () => {
content: (
<>
<EuiSpacer />
<StepPanel loading={loading} title={ruleI18n.ACTIONS}>
<StepPanel loading={loading}>
{actionsStep.data != null && (
<StepRuleActions
isReadOnlyView={false}

View file

@ -140,7 +140,7 @@ describe('rule helpers', () => {
enabled: true,
throttle: 'no_actions',
actions: [],
responseActions: [],
responseActions: undefined,
};
const aboutRuleDataDetailsData = {
note: '# this is some markdown documentation',
@ -411,7 +411,7 @@ describe('rule helpers', () => {
actionTypeId: 'action_type_id',
},
],
responseActions: [],
responseActions: undefined,
enabled: mockedRule.enabled,
throttle: 'no_actions',
};

View file

@ -76,7 +76,7 @@ export const getActionsStepsData = (
response_actions?: ResponseAction[];
}
): ActionsStepRule => {
const { enabled, throttle, meta, actions = [], response_actions: responseActions = [] } = rule;
const { enabled, throttle, meta, actions = [], response_actions: responseActions } = rule;
return {
actions: actions?.map(transformRuleToAlertAction),

View file

@ -11,7 +11,6 @@ import React, { useMemo } from 'react';
import deepEqual from 'fast-deep-equal';
import type { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import type { EntityType } from '@kbn/timelines-plugin/common';
import type { AlertRawEventData } from '../../../../common/components/event_details/event_details';
import type { BrowserFields } from '../../../../common/containers/source';
import { ExpandableEvent, ExpandableEventTitle } from './expandable_event';
import { useTimelineEventsDetails } from '../../../containers/details';
@ -128,7 +127,7 @@ const EventDetailsPanelComponent: React.FC<EventDetailsPanelProps> = ({
isIsolateActionSuccessBannerVisible={isIsolateActionSuccessBannerVisible}
isHostIsolationPanelOpen={isHostIsolationPanelOpen}
loading={loading}
rawEventData={rawEventData as AlertRawEventData}
rawEventData={rawEventData}
showAlertDetails={showAlertDetails}
timelineId={timelineId}
isReadOnly={isReadOnly}
@ -166,7 +165,7 @@ const EventDetailsPanelComponent: React.FC<EventDetailsPanelProps> = ({
isAlert={isAlert}
isDraggable={isDraggable}
loading={loading}
rawEventData={rawEventData as AlertRawEventData}
rawEventData={rawEventData}
timelineId={timelineId}
timelineTabType={tabType}
handleOnEventClosed={handleOnEventClosed}

View file

@ -10,27 +10,6 @@ import type { RuleResponseAction } from '../../../../common/detection_engine/rul
import { RESPONSE_ACTION_TYPES } from '../../../../common/detection_engine/rule_response_actions/schemas';
import type { SetupPlugins } from '../../../plugin_contract';
interface OsqueryQuery {
id: string;
query: string;
ecs_mapping: Record<string, Record<'field', string>>;
version: string;
interval?: number;
platform: string;
}
interface OsqueryResponseAction {
actionTypeId: RESPONSE_ACTION_TYPES.OSQUERY;
params: {
id: string;
queries: OsqueryQuery[];
savedQueryId: string;
query: string;
packId: string;
ecs_mapping?: Record<string, { field?: string; value?: string }>;
};
}
interface ScheduleNotificationActions {
signals: unknown[];
responseActions: RuleResponseAction[];
@ -42,10 +21,6 @@ interface IAlert {
};
}
const isOsqueryAction = (action: RuleResponseAction): action is OsqueryResponseAction => {
return action.actionTypeId === RESPONSE_ACTION_TYPES.OSQUERY;
};
export const scheduleNotificationResponseActions = (
{ signals, responseActions }: ScheduleNotificationActions,
osqueryCreateAction?: SetupPlugins['osquery']['osqueryCreateAction']
@ -55,14 +30,8 @@ export const scheduleNotificationResponseActions = (
const alertIds = map(filteredAlerts, '_id');
responseActions.forEach((responseAction) => {
if (isOsqueryAction(responseAction) && osqueryCreateAction) {
const {
savedQueryId,
packId,
queries,
ecs_mapping: ecsMapping,
...rest
} = responseAction.params;
if (responseAction.actionTypeId === RESPONSE_ACTION_TYPES.OSQUERY && osqueryCreateAction) {
const { savedQueryId, packId, queries, ecsMapping, ...rest } = responseAction.params;
return osqueryCreateAction({
...rest,

View file

@ -25194,6 +25194,7 @@
"xpack.securitySolution.detectionEngine.relatedIntegrations.popoverDescriptionInstalledVersionTooltip": "Non-correspondance de version ; veuillez résoudre le problème ! La version installée est \"{installedVersion}\" alors que la version exigée est \"{requiredVersion}\"",
"xpack.securitySolution.detectionEngine.relatedIntegrations.popoverTitle": "[{integrationsCount}] {integrationsCount, plural, =1 {Intégration associée disponible} other {Intégrations associées disponibles}}",
"xpack.securitySolution.detectionEngine.rule.editRule.errorMsgDescription": "Une entrée est incorrecte dans {countError, plural, one {cet onglet} other {ces onglets}} : {tabHasError}",
"xpack.securitySolution.detectionEngine.rule.editRule.actionSectionsTitle": "Actions",
"xpack.securitySolution.detectionEngine.ruleDetails.ruleCreationDescription": "Créé par : {by} le {date}",
"xpack.securitySolution.detectionEngine.ruleDetails.ruleExecutionLog.gapDurationColumnTooltip": "Durée de l'écart dans l'exécution de la règle (hh:mm:ss:SSS). Ajustez l'historique des règles ou {seeDocs} pour réduire les écarts.",
"xpack.securitySolution.detectionEngine.ruleDetails.ruleExecutionLog.searchLimitExceededLabel": "Plus de {totalItems} exceptions de règle correspondent aux filtres fournis. Affichage des {maxItems} premières en fonction du \"@timestamp\" le plus récent. Utilisez d'autres contraintes de filtres pour afficher des événements d'exécution supplémentaires.",

View file

@ -25170,6 +25170,7 @@
"xpack.securitySolution.detectionEngine.relatedIntegrations.popoverDescription": "{integrationsCount, plural, =1 {以下の統合} other {以下の1つ以上の統合}}をインストールおよび構成し、この検出ルールに必要なデータを取り込みます。",
"xpack.securitySolution.detectionEngine.relatedIntegrations.popoverDescriptionInstalledVersionTooltip": "バージョン不一致 - 解決してください。バージョン`{requiredVersion}`が必要なときのインストール済みバージョン`{installedVersion}`",
"xpack.securitySolution.detectionEngine.rule.editRule.errorMsgDescription": "{countError, plural, one {このタブ} other {これらのタブ}}に無効な入力があります:{tabHasError}",
"xpack.securitySolution.detectionEngine.rule.editRule.actionSectionsTitle": "アクション",
"xpack.securitySolution.detectionEngine.ruleDetails.ruleCreationDescription": "作成者:{by} 日付:{date}",
"xpack.securitySolution.detectionEngine.ruleDetails.ruleExecutionLog.gapDurationColumnTooltip": "ルール実行のギャップの期間hh:mm:ss:SSS。ルールルックバックを調整するか、ギャップの軽減については{seeDocs}してください。",
"xpack.securitySolution.detectionEngine.ruleDetails.ruleExecutionLog.searchLimitExceededLabel": "{totalItems}件以上のルール実行が指定されたフィルターと一致します。最新の「@timestamp」で最初の{maxItems}を表示しています。さらにフィルターを絞り込み、追加の実行イベントを表示します。",

View file

@ -25200,6 +25200,7 @@
"xpack.securitySolution.detectionEngine.relatedIntegrations.popoverDescriptionInstalledVersionTooltip": "版本不匹配 -- 请解决!已安装版本 `{installedVersion}`,而所需版本为 `{requiredVersion}`",
"xpack.securitySolution.detectionEngine.relatedIntegrations.popoverTitle": "有 [{integrationsCount}] 个相关{integrationsCount, plural, other {集成}}可用",
"xpack.securitySolution.detectionEngine.rule.editRule.errorMsgDescription": "您在{countError, plural, other {以下选项卡}}中的输入无效:{tabHasError}",
"xpack.securitySolution.detectionEngine.rule.editRule.actionSectionsTitle": "操作",
"xpack.securitySolution.detectionEngine.ruleDetails.ruleCreationDescription": "由 {by} 于 {date}创建",
"xpack.securitySolution.detectionEngine.ruleDetails.ruleExecutionLog.gapDurationColumnTooltip": "规则执行中缺口的持续时间 (hh:mm:ss:SSS)。调整规则回查或{seeDocs}以缩小缺口。",
"xpack.securitySolution.detectionEngine.ruleDetails.ruleExecutionLog.searchLimitExceededLabel": "超过 {totalItems} 个规则执行与提供的筛选相匹配。正在按最近的“@timestamp”显示前 {maxItems} 项。进一步限制筛选以查看其他执行事件。",

View file

@ -62,6 +62,7 @@ export interface ActionAccordionFormProps {
actionTypeRegistry: ActionTypeRegistryContract;
recoveryActionGroup?: string;
isActionGroupDisabledForActionType?: (actionGroupId: string, actionTypeId: string) => boolean;
hideActionHeader?: boolean;
}
interface ActiveActionConnectorState {
@ -85,6 +86,7 @@ export const ActionForm = ({
actionTypeRegistry,
recoveryActionGroup,
isActionGroupDisabledForActionType,
hideActionHeader,
}: ActionAccordionFormProps) => {
const {
http,
@ -295,15 +297,19 @@ export const ActionForm = ({
</SectionLoading>
) : (
<>
<EuiTitle size="s">
<h4>
<FormattedMessage
defaultMessage="Actions"
id="xpack.triggersActionsUI.sections.actionForm.actionSectionsTitle"
/>
</h4>
</EuiTitle>
<EuiSpacer size="m" />
{!hideActionHeader && (
<>
<EuiTitle size="s">
<h4>
<FormattedMessage
defaultMessage="Actions"
id="xpack.triggersActionsUI.sections.actionForm.actionSectionsTitle"
/>
</h4>
</EuiTitle>
<EuiSpacer size="m" />
</>
)}
{actionTypesIndex &&
actions.map((actionItem: RuleAction, index: number) => {
const actionConnector = connectors.find((field) => field.id === actionItem.id);

View file

@ -3521,6 +3521,10 @@
version "0.0.0"
uid ""
"@kbn/osquery-io-ts-types@link:bazel-bin/packages/kbn-osquery-io-ts-types":
version "0.0.0"
uid ""
"@kbn/performance-testing-dataset-extractor@link:bazel-bin/packages/kbn-performance-testing-dataset-extractor":
version "0.0.0"
uid ""
@ -7607,6 +7611,10 @@
version "0.0.0"
uid ""
"@types/kbn__osquery-io-ts-types@link:bazel-bin/packages/kbn-osquery-io-ts-types/npm_module_types":
version "0.0.0"
uid ""
"@types/kbn__performance-testing-dataset-extractor@link:bazel-bin/packages/kbn-performance-testing-dataset-extractor/npm_module_types":
version "0.0.0"
uid ""