diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/agent_status_filter.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/agent_status_filter.tsx
index ece89842c1bf6..9152b04970ba6 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/agent_status_filter.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/agent_status_filter.tsx
@@ -13,6 +13,7 @@ import {
EuiText,
EuiTourStep,
useEuiTheme,
+ EuiLink,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
@@ -64,17 +65,11 @@ const LeftpaddedNotificationBadge = styled(EuiNotificationBadge)`
margin-left: 10px;
`;
-const TourStepNoHeaderFooter = styled(EuiTourStep)`
- .euiTourFooter {
- display: none;
- }
- .euiTourHeader {
- display: none;
- }
-`;
-
-const InactiveAgentsTourStep: React.FC<{ isOpen: boolean }> = ({ children, isOpen }) => (
- void;
+}> = ({ children, isOpen, setInactiveAgentsCalloutHasBeenDismissed }) => (
+
= ({ children, isOpe
onFinish={() => {}}
anchorPosition="upCenter"
maxWidth={280}
+ footerAction={
+ {
+ setInactiveAgentsCalloutHasBeenDismissed(true);
+ }}
+ >
+
+
+ }
>
{children as React.ReactElement}
-
+
);
export const AgentStatusFilter: React.FC<{
@@ -160,6 +167,7 @@ export const AgentStatusFilter: React.FC<{
return (
0 && !inactiveAgentsCalloutHasBeenDismissed}
+ setInactiveAgentsCalloutHasBeenDismissed={setInactiveAgentsCalloutHasBeenDismissed}
>
void;
}
-export const AssetsPage = ({ packageInfo }: AssetsPanelProps) => {
+export const AssetsPage = ({ packageInfo, refetchPackageInfo }: AssetsPanelProps) => {
const { name, version } = packageInfo;
+
const pkgkey = `${name}-${version}`;
const { spaces, docLinks } = useStartServices();
const customAssetsExtension = useUIExtension(packageInfo.name, 'package-detail-assets');
@@ -60,6 +62,12 @@ export const AssetsPage = ({ packageInfo }: AssetsPanelProps) => {
const [fetchError, setFetchError] = useState();
const [isLoading, setIsLoading] = useState(true);
+ const forceRefreshAssets = useCallback(() => {
+ if (refetchPackageInfo) {
+ refetchPackageInfo();
+ }
+ }, [refetchPackageInfo]);
+
useEffect(() => {
const fetchAssetSavedObjects = async () => {
if ('installationInfo' in packageInfo) {
@@ -245,6 +253,7 @@ export const AssetsPage = ({ packageInfo }: AssetsPanelProps) => {
>
diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/deferred_assets_accordion.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/deferred_assets_accordion.tsx
index 4a10a360f31de..85f5984e7de64 100644
--- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/deferred_assets_accordion.tsx
+++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/deferred_assets_accordion.tsx
@@ -26,11 +26,13 @@ import { DeferredTransformAccordion } from './deferred_transforms_accordion';
interface Props {
packageInfo: PackageInfo;
deferredInstallations: EsAssetReference[];
+ forceRefreshAssets?: () => void;
}
export const DeferredAssetsSection: FunctionComponent = ({
deferredInstallations,
packageInfo,
+ forceRefreshAssets,
}) => {
const authz = useAuthz();
@@ -60,6 +62,7 @@ export const DeferredAssetsSection: FunctionComponent = ({
packageInfo={packageInfo}
type={ElasticsearchAssetType.transform}
deferredInstallations={deferredTransforms}
+ forceRefreshAssets={forceRefreshAssets}
/>
>
);
diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/deferred_transforms_accordion.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/deferred_transforms_accordion.tsx
index 42b39f966e836..ed76562ed44cb 100644
--- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/deferred_transforms_accordion.tsx
+++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/deferred_transforms_accordion.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import React, { Fragment, useCallback, useMemo, useState } from 'react';
+import React, { Fragment, useCallback, useState, useMemo } from 'react';
import type { FunctionComponent, MouseEvent } from 'react';
import {
@@ -42,6 +42,7 @@ interface Props {
packageInfo: PackageInfo;
type: ElasticsearchAssetType.transform;
deferredInstallations: EsAssetReference[];
+ forceRefreshAssets?: () => void;
}
export const getDeferredAssetDescription = (
@@ -83,6 +84,7 @@ export const DeferredTransformAccordion: FunctionComponent = ({
packageInfo,
type,
deferredInstallations,
+ forceRefreshAssets,
}) => {
const { notifications } = useStartServices();
const [isLoading, setIsLoading] = useState(false);
@@ -159,6 +161,9 @@ export const DeferredTransformAccordion: FunctionComponent = ({
),
{ toastLifeTimeMs: 1000 }
);
+ if (forceRefreshAssets) {
+ forceRefreshAssets();
+ }
}
}
} catch (e) {
@@ -171,11 +176,14 @@ export const DeferredTransformAccordion: FunctionComponent = ({
}
),
});
+ if (forceRefreshAssets) {
+ forceRefreshAssets();
+ }
}
}
setIsLoading(false);
},
- [notifications.toasts, packageInfo.name, packageInfo.version]
+ [notifications.toasts, packageInfo.name, packageInfo.version, forceRefreshAssets]
);
if (deferredTransforms.length === 0) return null;
return (
diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx
index b2a2ad23ba514..1312b21f49784 100644
--- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx
+++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx
@@ -776,7 +776,7 @@ export function Detail() {
-
+
diff --git a/x-pack/plugins/fleet/server/routes/epm/handlers.ts b/x-pack/plugins/fleet/server/routes/epm/handlers.ts
index 6fadeff5180c2..9080c33b8de0a 100644
--- a/x-pack/plugins/fleet/server/routes/epm/handlers.ts
+++ b/x-pack/plugins/fleet/server/routes/epm/handlers.ts
@@ -412,6 +412,8 @@ export const bulkInstallPackagesFromRegistryHandler: FleetRequestHandler<
const savedObjectsClient = fleetContext.internalSoClient;
const esClient = coreContext.elasticsearch.client.asInternalUser;
const spaceId = fleetContext.spaceId;
+ const user = (await appContextService.getSecurity()?.authc.getCurrentUser(request)) || undefined;
+ const authorizationHeader = HTTPAuthorizationHeader.parseFromRequest(request, user?.username);
const bulkInstalledResponses = await bulkInstallPackages({
savedObjectsClient,
@@ -420,6 +422,7 @@ export const bulkInstallPackagesFromRegistryHandler: FleetRequestHandler<
spaceId,
prerelease: request.query.prerelease,
force: request.body.force,
+ authorizationHeader,
});
const payload = bulkInstalledResponses.map(bulkInstallServiceResponseToHttpEntry);
const body: BulkInstallPackagesResponse = {
diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/install.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/install.ts
index 984a88d87e61f..0b801710e3ebe 100644
--- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/install.ts
+++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/install.ts
@@ -447,7 +447,6 @@ const installTransformsAssets = async (
})
: // No need to generate api key/secondary auth if all transforms are run as kibana_system user
undefined;
-
// delete all previous transform
await Promise.all([
deleteTransforms(
@@ -761,7 +760,9 @@ async function handleTransformInstall({
throw err;
}
}
- } else {
+ }
+
+ if (startTransform === false || transform?.content?.settings?.unattended === true) {
// if transform was not set to start automatically in yml config,
// we need to check using _stats if the transform had insufficient permissions
try {
@@ -773,7 +774,11 @@ async function handleTransformInstall({
),
{ logger, additionalResponseStatuses: [400] }
);
- if (Array.isArray(transformStats.transforms) && transformStats.transforms.length === 1) {
+ if (
+ transformStats &&
+ Array.isArray(transformStats.transforms) &&
+ transformStats.transforms.length === 1
+ ) {
const transformHealth = transformStats.transforms[0].health;
if (
transformHealth &&
diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/reauthorize.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/reauthorize.ts
index 7bba68d84bcf8..b3d4d388811b2 100644
--- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/reauthorize.ts
+++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/reauthorize.ts
@@ -10,6 +10,8 @@ import type { Logger } from '@kbn/logging';
import type { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server';
import { sortBy, uniqBy } from 'lodash';
+import { isPopulatedObject } from '@kbn/ml-is-populated-object';
+import type { ErrorResponseBase } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import type { SecondaryAuthorizationHeader } from '../../../../../common/types/models/transform_api_key';
import { updateEsAssetReferences } from '../../packages/install';
@@ -30,6 +32,9 @@ interface FleetTransformMetadata {
transformId: string;
}
+const isErrorResponse = (arg: unknown): arg is ErrorResponseBase =>
+ isPopulatedObject(arg, ['error']);
+
async function reauthorizeAndStartTransform({
esClient,
logger,
@@ -68,6 +73,19 @@ async function reauthorizeAndStartTransform({
() => esClient.transform.startTransform({ transform_id: transformId }, { ignore: [409] }),
{ logger, additionalResponseStatuses: [400] }
);
+
+ // Transform can already be started even without sufficient permission if 'unattended: true'
+ // So we are just catching that special case to showcase in the UI
+ // If unattended, calling _start will return a successful response, but with the error message in the body
+ if (
+ isErrorResponse(startedTransform) &&
+ startedTransform.status === 409 &&
+ Array.isArray(startedTransform.error?.root_cause) &&
+ startedTransform.error.root_cause[0]?.reason?.includes('already started')
+ ) {
+ return { transformId, success: true, error: null };
+ }
+
logger.debug(`Started transform: ${transformId}`);
return { transformId, success: startedTransform.acknowledged, error: null };
} catch (err) {
diff --git a/x-pack/plugins/fleet/server/services/epm/packages/get_bulk_assets.ts b/x-pack/plugins/fleet/server/services/epm/packages/get_bulk_assets.ts
index ebeaed684c3a0..8e66dc904dbf2 100644
--- a/x-pack/plugins/fleet/server/services/epm/packages/get_bulk_assets.ts
+++ b/x-pack/plugins/fleet/server/services/epm/packages/get_bulk_assets.ts
@@ -37,8 +37,8 @@ export async function getBulkAssets(
type: obj.type as unknown as ElasticsearchAssetType | KibanaSavedObjectType,
updatedAt: obj.updated_at,
attributes: {
- title: obj.attributes.title,
- description: obj.attributes.description,
+ title: obj.attributes?.title,
+ description: obj.attributes?.description,
},
};
});
diff --git a/x-pack/plugins/fleet/server/services/epm/packages/remove.ts b/x-pack/plugins/fleet/server/services/epm/packages/remove.ts
index ba9bace6a0dee..436c2efaa2275 100644
--- a/x-pack/plugins/fleet/server/services/epm/packages/remove.ts
+++ b/x-pack/plugins/fleet/server/services/epm/packages/remove.ts
@@ -190,24 +190,35 @@ async function deleteAssets(
// must delete index templates first, or component templates which reference them cannot be deleted
// must delete ingestPipelines first, or ml models referenced in them cannot be deleted.
// separate the assets into Index Templates and other assets.
- type Tuple = [EsAssetReference[], EsAssetReference[], EsAssetReference[]];
- const [indexTemplatesAndPipelines, indexAssets, otherAssets] = installedEs.reduce(
- ([indexTemplateAndPipelineTypes, indexAssetTypes, otherAssetTypes], asset) => {
- if (
- asset.type === ElasticsearchAssetType.indexTemplate ||
- asset.type === ElasticsearchAssetType.ingestPipeline
- ) {
- indexTemplateAndPipelineTypes.push(asset);
- } else if (asset.type === ElasticsearchAssetType.index) {
- indexAssetTypes.push(asset);
- } else {
- otherAssetTypes.push(asset);
- }
-
- return [indexTemplateAndPipelineTypes, indexAssetTypes, otherAssetTypes];
- },
- [[], [], []]
- );
+ type Tuple = [EsAssetReference[], EsAssetReference[], EsAssetReference[], EsAssetReference[]];
+ const [indexTemplatesAndPipelines, indexAssets, transformAssets, otherAssets] =
+ installedEs.reduce(
+ (
+ [indexTemplateAndPipelineTypes, indexAssetTypes, transformAssetTypes, otherAssetTypes],
+ asset
+ ) => {
+ if (
+ asset.type === ElasticsearchAssetType.indexTemplate ||
+ asset.type === ElasticsearchAssetType.ingestPipeline
+ ) {
+ indexTemplateAndPipelineTypes.push(asset);
+ } else if (asset.type === ElasticsearchAssetType.index) {
+ indexAssetTypes.push(asset);
+ } else if (asset.type === ElasticsearchAssetType.transform) {
+ transformAssetTypes.push(asset);
+ } else {
+ otherAssetTypes.push(asset);
+ }
+
+ return [
+ indexTemplateAndPipelineTypes,
+ indexAssetTypes,
+ transformAssetTypes,
+ otherAssetTypes,
+ ];
+ },
+ [[], [], [], []]
+ );
try {
// must first unset any default pipeline associated with any existing indices
@@ -215,7 +226,12 @@ async function deleteAssets(
await Promise.all(
indexAssets.map((asset) => updateIndexSettings(esClient, asset.id, { default_pipeline: '' }))
);
- // must delete index templates and pipelines first
+
+ // in case transform's destination index contains any pipline,
+ // we should delete the transforms first
+ await Promise.all(deleteESAssets(transformAssets, esClient));
+
+ // then delete index templates and pipelines
await Promise.all(deleteESAssets(indexTemplatesAndPipelines, esClient));
// then the other asset types
await Promise.all([
diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/import_rules/rule_to_import.mock.ts b/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/import_rules/rule_to_import.mock.ts
index 6161e2a00f960..2b36645363edc 100644
--- a/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/import_rules/rule_to_import.mock.ts
+++ b/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/import_rules/rule_to_import.mock.ts
@@ -7,17 +7,19 @@
import type { RuleToImport } from './rule_to_import';
-export const getImportRulesSchemaMock = (ruleId = 'rule-1'): RuleToImport => ({
- description: 'some description',
- name: 'Query with a rule id',
- query: 'user.name: root or user.name: admin',
- severity: 'high',
- type: 'query',
- risk_score: 55,
- language: 'kuery',
- rule_id: ruleId,
- immutable: false,
-});
+export const getImportRulesSchemaMock = (rewrites?: Partial): RuleToImport =>
+ ({
+ description: 'some description',
+ name: 'Query with a rule id',
+ query: 'user.name: root or user.name: admin',
+ severity: 'high',
+ type: 'query',
+ risk_score: 55,
+ language: 'kuery',
+ rule_id: 'rule-1',
+ immutable: false,
+ ...rewrites,
+ } as RuleToImport);
export const getImportRulesWithIdSchemaMock = (ruleId = 'rule-1'): RuleToImport => ({
id: '6afb8ce1-ea94-4790-8653-fd0b021d2113',
@@ -47,42 +49,46 @@ export const rulesToNdJsonString = (rules: RuleToImport[]) => {
* @param ruleIds Array of ruleIds with which to generate rule JSON
*/
export const ruleIdsToNdJsonString = (ruleIds: string[]) => {
- const rules = ruleIds.map((ruleId) => getImportRulesSchemaMock(ruleId));
+ const rules = ruleIds.map((ruleId) => getImportRulesSchemaMock({ rule_id: ruleId }));
return rulesToNdJsonString(rules);
};
-export const getImportThreatMatchRulesSchemaMock = (ruleId = 'rule-1'): RuleToImport => ({
- description: 'some description',
- name: 'Query with a rule id',
- query: 'user.name: root or user.name: admin',
- severity: 'high',
- type: 'threat_match',
- risk_score: 55,
- language: 'kuery',
- rule_id: ruleId,
- threat_index: ['index-123'],
- threat_mapping: [{ entries: [{ field: 'host.name', type: 'mapping', value: 'host.name' }] }],
- threat_query: '*:*',
- threat_filters: [
- {
- bool: {
- must: [
- {
- query_string: {
- query: 'host.name: linux',
- analyze_wildcard: true,
- time_zone: 'Zulu',
+export const getImportThreatMatchRulesSchemaMock = (
+ rewrites?: Partial
+): RuleToImport =>
+ ({
+ description: 'some description',
+ name: 'Query with a rule id',
+ query: 'user.name: root or user.name: admin',
+ severity: 'high',
+ type: 'threat_match',
+ risk_score: 55,
+ language: 'kuery',
+ rule_id: 'rule-1',
+ threat_index: ['index-123'],
+ threat_mapping: [{ entries: [{ field: 'host.name', type: 'mapping', value: 'host.name' }] }],
+ threat_query: '*:*',
+ threat_filters: [
+ {
+ bool: {
+ must: [
+ {
+ query_string: {
+ query: 'host.name: linux',
+ analyze_wildcard: true,
+ time_zone: 'Zulu',
+ },
},
- },
- ],
- filter: [],
- should: [],
- must_not: [],
+ ],
+ filter: [],
+ should: [],
+ must_not: [],
+ },
},
- },
- ],
- immutable: false,
-});
+ ],
+ immutable: false,
+ ...rewrites,
+ } as RuleToImport);
export const webHookConnector = {
id: 'cabc78e0-9031-11ed-b076-53cc4d57aaf1',
@@ -104,8 +110,7 @@ export const webHookConnector = {
export const ruleWithConnectorNdJSON = (): string => {
const items = [
- {
- ...getImportRulesSchemaMock(),
+ getImportRulesSchemaMock({
actions: [
{
group: 'default',
@@ -114,7 +119,7 @@ export const ruleWithConnectorNdJSON = (): string => {
params: {},
},
],
- },
+ }),
webHookConnector,
];
const stringOfExceptions = items.map((item) => JSON.stringify(item));
diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/import_rules/rule_to_import.test.ts b/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/import_rules/rule_to_import.test.ts
index 3f364c6619db6..2894da32593fa 100644
--- a/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/import_rules/rule_to_import.test.ts
+++ b/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/import_rules/rule_to_import.test.ts
@@ -27,10 +27,10 @@ describe('RuleToImport', () => {
});
test('extra properties are removed', () => {
- const payload: RuleToImportInput & { madeUp: string } = {
- ...getImportRulesSchemaMock(),
+ const payload: RuleToImportInput = getImportRulesSchemaMock({
+ // @ts-expect-error add an unknown field
madeUp: 'hi',
- };
+ });
const result = RuleToImport.safeParse(payload);
expectParseSuccess(result);
@@ -241,10 +241,7 @@ describe('RuleToImport', () => {
});
test('You can send in an empty array to threat', () => {
- const payload: RuleToImportInput = {
- ...getImportRulesSchemaMock(),
- threat: [],
- };
+ const payload: RuleToImportInput = getImportRulesSchemaMock({ threat: [] });
const result = RuleToImport.safeParse(payload);
@@ -289,10 +286,7 @@ describe('RuleToImport', () => {
});
test('allows references to be sent as valid', () => {
- const payload: RuleToImportInput = {
- ...getImportRulesSchemaMock(),
- references: ['index-1'],
- };
+ const payload: RuleToImportInput = getImportRulesSchemaMock({ references: ['index-1'] });
const result = RuleToImport.safeParse(payload);
@@ -307,10 +301,10 @@ describe('RuleToImport', () => {
});
test('references cannot be numbers', () => {
- const payload: Omit & { references: number[] } = {
- ...getImportRulesSchemaMock(),
+ const payload = getImportRulesSchemaMock({
+ // @ts-expect-error assign wrong type value
references: [5],
- };
+ });
const result = RuleToImport.safeParse(payload);
expectParseError(result);
@@ -321,10 +315,10 @@ describe('RuleToImport', () => {
});
test('indexes cannot be numbers', () => {
- const payload: Omit & { index: number[] } = {
- ...getImportRulesSchemaMock(),
+ const payload = getImportRulesSchemaMock({
+ // @ts-expect-error assign wrong type value
index: [5],
- };
+ });
const result = RuleToImport.safeParse(payload);
expectParseError(result);
@@ -358,10 +352,9 @@ describe('RuleToImport', () => {
});
test('saved_query type can have filters with it', () => {
- const payload = {
- ...getImportRulesSchemaMock(),
+ const payload = getImportRulesSchemaMock({
filters: [],
- };
+ });
const result = RuleToImport.safeParse(payload);
@@ -369,10 +362,10 @@ describe('RuleToImport', () => {
});
test('filters cannot be a string', () => {
- const payload: Omit & { filters: string } = {
- ...getImportRulesSchemaMock(),
+ const payload = getImportRulesSchemaMock({
+ // @ts-expect-error assign wrong type value
filters: 'some string',
- };
+ });
const result = RuleToImport.safeParse(payload);
expectParseError(result);
@@ -383,10 +376,7 @@ describe('RuleToImport', () => {
});
test('language validates with kuery', () => {
- const payload = {
- ...getImportRulesSchemaMock(),
- language: 'kuery',
- };
+ const payload = getImportRulesSchemaMock({ language: 'kuery' });
const result = RuleToImport.safeParse(payload);
@@ -394,10 +384,7 @@ describe('RuleToImport', () => {
});
test('language validates with lucene', () => {
- const payload = {
- ...getImportRulesSchemaMock(),
- language: 'lucene',
- };
+ const payload = getImportRulesSchemaMock({ language: 'lucene' });
const result = RuleToImport.safeParse(payload);
@@ -405,10 +392,10 @@ describe('RuleToImport', () => {
});
test('language does not validate with something made up', () => {
- const payload: Omit & { language: string } = {
- ...getImportRulesSchemaMock(),
+ const payload = getImportRulesSchemaMock({
+ // @ts-expect-error assign unsupported value
language: 'something-made-up',
- };
+ });
const result = RuleToImport.safeParse(payload);
expectParseError(result);
@@ -419,10 +406,7 @@ describe('RuleToImport', () => {
});
test('max_signals cannot be negative', () => {
- const payload: RuleToImportInput = {
- ...getImportRulesSchemaMock(),
- max_signals: -1,
- };
+ const payload: RuleToImportInput = getImportRulesSchemaMock({ max_signals: -1 });
const result = RuleToImport.safeParse(payload);
expectParseError(result);
@@ -433,10 +417,7 @@ describe('RuleToImport', () => {
});
test('max_signals cannot be zero', () => {
- const payload: RuleToImportInput = {
- ...getImportRulesSchemaMock(),
- max_signals: 0,
- };
+ const payload: RuleToImportInput = getImportRulesSchemaMock({ max_signals: 0 });
const result = RuleToImport.safeParse(payload);
expectParseError(result);
@@ -447,10 +428,7 @@ describe('RuleToImport', () => {
});
test('max_signals can be 1', () => {
- const payload: RuleToImportInput = {
- ...getImportRulesSchemaMock(),
- max_signals: 1,
- };
+ const payload: RuleToImportInput = getImportRulesSchemaMock({ max_signals: 1 });
const result = RuleToImport.safeParse(payload);
@@ -458,10 +436,7 @@ describe('RuleToImport', () => {
});
test('You can optionally send in an array of tags', () => {
- const payload: RuleToImportInput = {
- ...getImportRulesSchemaMock(),
- tags: ['tag_1', 'tag_2'],
- };
+ const payload: RuleToImportInput = getImportRulesSchemaMock({ tags: ['tag_1', 'tag_2'] });
const result = RuleToImport.safeParse(payload);
@@ -469,10 +444,10 @@ describe('RuleToImport', () => {
});
test('You cannot send in an array of tags that are numbers', () => {
- const payload: Omit & { tags: number[] } = {
- ...getImportRulesSchemaMock(),
+ const payload = getImportRulesSchemaMock({
+ // @ts-expect-error assign unsupported value
tags: [0, 1, 2],
- };
+ });
const result = RuleToImport.safeParse(payload);
expectParseError(result);
@@ -483,11 +458,9 @@ describe('RuleToImport', () => {
});
test('You cannot send in an array of threat that are missing "framework"', () => {
- const payload: Omit & {
- threat: Array>>;
- } = {
- ...getImportRulesSchemaMock(),
+ const payload = getImportRulesSchemaMock({
threat: [
+ // @ts-expect-error assign unsupported value
{
tactic: {
id: 'fakeId',
@@ -503,7 +476,7 @@ describe('RuleToImport', () => {
],
},
],
- };
+ });
const result = RuleToImport.safeParse(payload);
expectParseError(result);
@@ -512,11 +485,9 @@ describe('RuleToImport', () => {
});
test('You cannot send in an array of threat that are missing "tactic"', () => {
- const payload: Omit & {
- threat: Array>>;
- } = {
- ...getImportRulesSchemaMock(),
+ const payload = getImportRulesSchemaMock({
threat: [
+ // @ts-expect-error assign unsupported value
{
framework: 'fake',
technique: [
@@ -528,7 +499,7 @@ describe('RuleToImport', () => {
],
},
],
- };
+ });
const result = RuleToImport.safeParse(payload);
expectParseError(result);
@@ -537,10 +508,7 @@ describe('RuleToImport', () => {
});
test('You can send in an array of threat that are missing "technique"', () => {
- const payload: Omit & {
- threat: Array>>;
- } = {
- ...getImportRulesSchemaMock(),
+ const payload = getImportRulesSchemaMock({
threat: [
{
framework: 'fake',
@@ -551,7 +519,7 @@ describe('RuleToImport', () => {
},
},
],
- };
+ });
const result = RuleToImport.safeParse(payload);
@@ -559,10 +527,9 @@ describe('RuleToImport', () => {
});
test('You can optionally send in an array of false positives', () => {
- const payload: RuleToImportInput = {
- ...getImportRulesSchemaMock(),
+ const payload: RuleToImportInput = getImportRulesSchemaMock({
false_positives: ['false_1', 'false_2'],
- };
+ });
const result = RuleToImport.safeParse(payload);
@@ -570,10 +537,10 @@ describe('RuleToImport', () => {
});
test('You cannot send in an array of false positives that are numbers', () => {
- const payload: Omit & { false_positives: number[] } = {
- ...getImportRulesSchemaMock(),
+ const payload = getImportRulesSchemaMock({
+ // @ts-expect-error assign unsupported value
false_positives: [5, 4],
- };
+ });
const result = RuleToImport.safeParse(payload);
expectParseError(result);
@@ -584,10 +551,10 @@ describe('RuleToImport', () => {
});
test('You cannot set the immutable to a number when trying to create a rule', () => {
- const payload: Omit & { immutable: number } = {
- ...getImportRulesSchemaMock(),
+ const payload = getImportRulesSchemaMock({
+ // @ts-expect-error assign unsupported value
immutable: 5,
- };
+ });
const result = RuleToImport.safeParse(payload);
expectParseError(result);
@@ -598,10 +565,9 @@ describe('RuleToImport', () => {
});
test('You can optionally set the immutable to be false', () => {
- const payload: RuleToImportInput = {
- ...getImportRulesSchemaMock(),
+ const payload: RuleToImportInput = getImportRulesSchemaMock({
immutable: false,
- };
+ });
const result = RuleToImport.safeParse(payload);
@@ -609,10 +575,10 @@ describe('RuleToImport', () => {
});
test('You cannot set the immutable to be true', () => {
- const payload: Omit & { immutable: true } = {
- ...getImportRulesSchemaMock(),
+ const payload = getImportRulesSchemaMock({
+ // @ts-expect-error assign unsupported value
immutable: true,
- };
+ });
const result = RuleToImport.safeParse(payload);
expectParseError(result);
@@ -623,10 +589,10 @@ describe('RuleToImport', () => {
});
test('You cannot set the immutable to be a number', () => {
- const payload: Omit & { immutable: number } = {
- ...getImportRulesSchemaMock(),
+ const payload = getImportRulesSchemaMock({
+ // @ts-expect-error assign unsupported value
immutable: 5,
- };
+ });
const result = RuleToImport.safeParse(payload);
expectParseError(result);
@@ -637,10 +603,9 @@ describe('RuleToImport', () => {
});
test('You cannot set the risk_score to 101', () => {
- const payload: RuleToImportInput = {
- ...getImportRulesSchemaMock(),
+ const payload: RuleToImportInput = getImportRulesSchemaMock({
risk_score: 101,
- };
+ });
const result = RuleToImport.safeParse(payload);
expectParseError(result);
@@ -651,10 +616,9 @@ describe('RuleToImport', () => {
});
test('You cannot set the risk_score to -1', () => {
- const payload: RuleToImportInput = {
- ...getImportRulesSchemaMock(),
+ const payload: RuleToImportInput = getImportRulesSchemaMock({
risk_score: -1,
- };
+ });
const result = RuleToImport.safeParse(payload);
expectParseError(result);
@@ -665,10 +629,9 @@ describe('RuleToImport', () => {
});
test('You can set the risk_score to 0', () => {
- const payload: RuleToImportInput = {
- ...getImportRulesSchemaMock(),
+ const payload: RuleToImportInput = getImportRulesSchemaMock({
risk_score: 0,
- };
+ });
const result = RuleToImport.safeParse(payload);
@@ -676,10 +639,9 @@ describe('RuleToImport', () => {
});
test('You can set the risk_score to 100', () => {
- const payload: RuleToImportInput = {
- ...getImportRulesSchemaMock(),
+ const payload: RuleToImportInput = getImportRulesSchemaMock({
risk_score: 100,
- };
+ });
const result = RuleToImport.safeParse(payload);
@@ -687,12 +649,11 @@ describe('RuleToImport', () => {
});
test('You can set meta to any object you want', () => {
- const payload: RuleToImportInput = {
- ...getImportRulesSchemaMock(),
+ const payload: RuleToImportInput = getImportRulesSchemaMock({
meta: {
somethingMadeUp: { somethingElse: true },
},
- };
+ });
const result = RuleToImport.safeParse(payload);
@@ -700,10 +661,10 @@ describe('RuleToImport', () => {
});
test('You cannot create meta as a string', () => {
- const payload: Omit & { meta: string } = {
- ...getImportRulesSchemaMock(),
+ const payload = getImportRulesSchemaMock({
+ // @ts-expect-error assign unsupported value
meta: 'should not work',
- };
+ });
const result = RuleToImport.safeParse(payload);
expectParseError(result);
@@ -714,11 +675,10 @@ describe('RuleToImport', () => {
});
test('validates with timeline_id and timeline_title', () => {
- const payload: RuleToImportInput = {
- ...getImportRulesSchemaMock(),
+ const payload: RuleToImportInput = getImportRulesSchemaMock({
timeline_id: 'timeline-id',
timeline_title: 'timeline-title',
- };
+ });
const result = RuleToImport.safeParse(payload);
@@ -726,10 +686,9 @@ describe('RuleToImport', () => {
});
test('rule_id is required and you cannot get by with just id', () => {
- const payload: RuleToImportInput = {
- ...getImportRulesSchemaMock(),
+ const payload: RuleToImportInput = getImportRulesSchemaMock({
id: 'c4e80a0d-e20f-4efc-84c1-08112da5a612',
- };
+ });
// @ts-expect-error
delete payload.rule_id;
@@ -740,13 +699,12 @@ describe('RuleToImport', () => {
});
test('it validates with created_at, updated_at, created_by, updated_by values', () => {
- const payload: RuleToImportInput = {
- ...getImportRulesSchemaMock(),
+ const payload: RuleToImportInput = getImportRulesSchemaMock({
created_at: '2020-01-09T06:15:24.749Z',
updated_at: '2020-01-09T06:15:24.749Z',
created_by: 'Braden Hassanabad',
updated_by: 'Evan Hassanabad',
- };
+ });
const result = RuleToImport.safeParse(payload);
@@ -754,10 +712,7 @@ describe('RuleToImport', () => {
});
test('it does not validate with epoch strings for created_at', () => {
- const payload: RuleToImportInput = {
- ...getImportRulesSchemaMock(),
- created_at: '1578550728650',
- };
+ const payload: RuleToImportInput = getImportRulesSchemaMock({ created_at: '1578550728650' });
const result = RuleToImport.safeParse(payload);
expectParseError(result);
@@ -766,10 +721,7 @@ describe('RuleToImport', () => {
});
test('it does not validate with epoch strings for updated_at', () => {
- const payload: RuleToImportInput = {
- ...getImportRulesSchemaMock(),
- updated_at: '1578550728650',
- };
+ const payload: RuleToImportInput = getImportRulesSchemaMock({ updated_at: '1578550728650' });
const result = RuleToImport.safeParse(payload);
expectParseError(result);
@@ -800,10 +752,10 @@ describe('RuleToImport', () => {
});
test('You cannot set the severity to a value other than low, medium, high, or critical', () => {
- const payload: Omit & { severity: string } = {
- ...getImportRulesSchemaMock(),
+ const payload = getImportRulesSchemaMock({
+ // @ts-expect-error assign unsupported value
severity: 'junk',
- };
+ });
const result = RuleToImport.safeParse(payload);
expectParseError(result);
@@ -825,10 +777,12 @@ describe('RuleToImport', () => {
});
test('You cannot send in an array of actions that are missing "group"', () => {
- const payload: Omit = {
- ...getImportRulesSchemaMock(),
- actions: [{ id: 'id', action_type_id: 'action_type_id', params: {} }],
- };
+ const payload = getImportRulesSchemaMock({
+ actions: [
+ // @ts-expect-error assign unsupported value
+ { id: 'id', action_type_id: 'action_type_id', params: {} },
+ ],
+ });
const result = RuleToImport.safeParse(payload);
expectParseError(result);
@@ -837,10 +791,12 @@ describe('RuleToImport', () => {
});
test('You cannot send in an array of actions that are missing "id"', () => {
- const payload: Omit = {
- ...getImportRulesSchemaMock(),
- actions: [{ group: 'group', action_type_id: 'action_type_id', params: {} }],
- };
+ const payload = getImportRulesSchemaMock({
+ actions: [
+ // @ts-expect-error assign unsupported value
+ { group: 'group', action_type_id: 'action_type_id', params: {} },
+ ],
+ });
const result = RuleToImport.safeParse(payload);
expectParseError(result);
@@ -849,10 +805,12 @@ describe('RuleToImport', () => {
});
test('You cannot send in an array of actions that are missing "action_type_id"', () => {
- const payload: Omit = {
- ...getImportRulesSchemaMock(),
- actions: [{ group: 'group', id: 'id', params: {} }],
- };
+ const payload = getImportRulesSchemaMock({
+ actions: [
+ // @ts-expect-error assign unsupported value
+ { group: 'group', id: 'id', params: {} },
+ ],
+ });
const result = RuleToImport.safeParse(payload);
expectParseError(result);
@@ -863,10 +821,12 @@ describe('RuleToImport', () => {
});
test('You cannot send in an array of actions that are missing "params"', () => {
- const payload: Omit = {
- ...getImportRulesSchemaMock(),
- actions: [{ group: 'group', id: 'id', action_type_id: 'action_type_id' }],
- };
+ const payload = getImportRulesSchemaMock({
+ actions: [
+ // @ts-expect-error assign unsupported value
+ { group: 'group', id: 'id', action_type_id: 'action_type_id' },
+ ],
+ });
const result = RuleToImport.safeParse(payload);
expectParseError(result);
@@ -875,17 +835,17 @@ describe('RuleToImport', () => {
});
test('You cannot send in an array of actions that are including "actionTypeId"', () => {
- const payload: Omit = {
- ...getImportRulesSchemaMock(),
+ const payload = getImportRulesSchemaMock({
actions: [
{
group: 'group',
id: 'id',
+ // @ts-expect-error assign unsupported value
actionTypeId: 'actionTypeId',
params: {},
},
],
- };
+ });
const result = RuleToImport.safeParse(payload);
expectParseError(result);
@@ -907,32 +867,28 @@ describe('RuleToImport', () => {
describe('note', () => {
test('You can set note to a string', () => {
- const payload: RuleToImport = {
- ...getImportRulesSchemaMock(),
+ const payload: RuleToImport = getImportRulesSchemaMock({
note: '# documentation markdown here',
- };
+ });
const result = RuleToImport.safeParse(payload);
expectParseSuccess(result);
});
test('You can set note to an empty string', () => {
- const payload: RuleToImportInput = {
- ...getImportRulesSchemaMock(),
- note: '',
- };
+ const payload: RuleToImportInput = getImportRulesSchemaMock({ note: '' });
const result = RuleToImport.safeParse(payload);
expectParseSuccess(result);
});
test('You cannot create note as an object', () => {
- const payload: Omit & { note: {} } = {
- ...getImportRulesSchemaMock(),
+ const payload = getImportRulesSchemaMock({
+ // @ts-expect-error assign unsupported value
note: {
somethingHere: 'something else',
},
- };
+ });
const result = RuleToImport.safeParse(payload);
expectParseError(result);
@@ -1102,10 +1058,10 @@ describe('RuleToImport', () => {
});
test('data_view_id cannot be a number', () => {
- const payload: Omit & { data_view_id: number } = {
- ...getImportRulesSchemaMock(),
+ const payload = getImportRulesSchemaMock({
+ // @ts-expect-error assign unsupported value
data_view_id: 5,
- };
+ });
const result = RuleToImport.safeParse(payload);
expectParseError(result);
diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/import_rules/rule_to_import_validation.test.ts b/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/import_rules/rule_to_import_validation.test.ts
index 31ac993eb4053..597dcf0cc3bcb 100644
--- a/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/import_rules/rule_to_import_validation.test.ts
+++ b/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/import_rules/rule_to_import_validation.test.ts
@@ -12,40 +12,34 @@ import { validateRuleToImport } from './rule_to_import_validation';
describe('Rule to import schema, additional validation', () => {
describe('validateRuleToImport', () => {
test('You cannot omit timeline_title when timeline_id is present', () => {
- const schema: RuleToImport = {
- ...getImportRulesSchemaMock(),
+ const schema: RuleToImport = getImportRulesSchemaMock({
timeline_id: '123',
- };
+ });
delete schema.timeline_title;
const errors = validateRuleToImport(schema);
expect(errors).toEqual(['when "timeline_id" exists, "timeline_title" must also exist']);
});
test('You cannot have empty string for timeline_title when timeline_id is present', () => {
- const schema: RuleToImport = {
- ...getImportRulesSchemaMock(),
+ const schema: RuleToImport = getImportRulesSchemaMock({
timeline_id: '123',
timeline_title: '',
- };
+ });
const errors = validateRuleToImport(schema);
expect(errors).toEqual(['"timeline_title" cannot be an empty string']);
});
test('You cannot have timeline_title with an empty timeline_id', () => {
- const schema: RuleToImport = {
- ...getImportRulesSchemaMock(),
+ const schema: RuleToImport = getImportRulesSchemaMock({
timeline_id: '',
timeline_title: 'some-title',
- };
+ });
const errors = validateRuleToImport(schema);
expect(errors).toEqual(['"timeline_id" cannot be an empty string']);
});
test('You cannot have timeline_title without timeline_id', () => {
- const schema: RuleToImport = {
- ...getImportRulesSchemaMock(),
- timeline_title: 'some-title',
- };
+ const schema: RuleToImport = getImportRulesSchemaMock({ timeline_title: 'some-title' });
delete schema.timeline_id;
const errors = validateRuleToImport(schema);
expect(errors).toEqual(['when "timeline_title" exists, "timeline_id" must also exist']);
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx
index e8a97e974431a..a9b6d63d852d4 100644
--- a/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx
+++ b/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx
@@ -163,6 +163,14 @@ const StyledMinHeightTabContainer = styled.div`
min-height: 800px;
`;
+/**
+ * Wrapper for the About, Definition and Schedule sections.
+ * - Allows for overflow wrapping of extremely long text, that might otherwise break the layout.
+ */
+const RuleFieldsSectionWrapper = styled.div`
+ overflow-wrap: anywhere;
+`;
+
type DetectionEngineComponentProps = PropsFromRedux;
const RuleDetailsPageComponent: React.FC = ({
@@ -648,50 +656,52 @@ const RuleDetailsPageComponent: React.FC = ({
{ruleError}
-
-
- {rule !== null && (
-
- )}
-
-
-
-
-
-
- {rule !== null && !isStartingJobs && (
-
- )}
-
-
-
-
-
- {rule != null && }
-
-
- {hasActions && (
-
-
-
+
+
+
+ {rule !== null && (
+
+ )}
+
+
+
+
+
+
+ {rule !== null && !isStartingJobs && (
+
+ )}
- )}
-
-
-
+
+
+
+ {rule != null && }
+
+
+ {hasActions && (
+
+
+
+
+
+ )}
+
+
+
+
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_definition_section.styles.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_definition_section.styles.ts
new file mode 100644
index 0000000000000..30439722cd18b
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_definition_section.styles.ts
@@ -0,0 +1,45 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { useEuiTheme } from '@elastic/eui';
+import { css } from '@emotion/css';
+import { useMemo } from 'react';
+
+export const useFiltersStyles = () => {
+ return useMemo(
+ () => ({
+ flexGroup: css`
+ max-width: 600px;
+ `,
+ }),
+ []
+ );
+};
+
+export const useQueryStyles = () => {
+ return useMemo(
+ () => ({
+ content: css`
+ white-space: pre-wrap;
+ `,
+ }),
+ []
+ );
+};
+
+export const useRequiredFieldsStyles = () => {
+ const { euiTheme } = useEuiTheme();
+ return useMemo(
+ () => ({
+ fieldTypeText: css({
+ fontFamily: euiTheme.font.familyCode,
+ display: 'inline',
+ }),
+ }),
+ [euiTheme.font.familyCode]
+ );
+};
diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_definition_section.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_definition_section.tsx
index cb37caa661fda..ec8a557459c26 100644
--- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_definition_section.tsx
+++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_definition_section.tsx
@@ -7,7 +7,6 @@
import React from 'react';
import { isEmpty } from 'lodash/fp';
-import styled from 'styled-components';
import {
EuiDescriptionList,
EuiText,
@@ -15,7 +14,6 @@ import {
EuiFlexItem,
EuiFlexGroup,
EuiLoadingSpinner,
- EuiBadge,
} from '@elastic/eui';
import type { EuiDescriptionListProps } from '@elastic/eui';
import type {
@@ -25,9 +23,10 @@ import type {
import type { Filter } from '@kbn/es-query';
import type { SavedQuery } from '@kbn/data-plugin/public';
import { mapAndFlattenFilters } from '@kbn/data-plugin/public';
+import type { DataView } from '@kbn/data-views-plugin/public';
import { FieldIcon } from '@kbn/react-field';
import { castEsToKbnFieldTypeName } from '@kbn/field-types';
-import { FilterBadgeGroup } from '@kbn/unified-search-plugin/public';
+import { FilterItems } from '@kbn/unified-search-plugin/public';
import type {
AlertSuppressionMissingFieldsStrategy,
RequiredFieldArray,
@@ -55,6 +54,11 @@ import { BadgeList } from './badge_list';
import { DEFAULT_DESCRIPTION_LIST_COLUMN_WIDTHS } from './constants';
import * as i18n from './translations';
import type { ExperimentalFeatures } from '../../../../../common/experimental_features';
+import {
+ useFiltersStyles,
+ useQueryStyles,
+ useRequiredFieldsStyles,
+} from './rule_definition_section.styles';
interface SavedQueryNameProps {
savedQueryName: string;
@@ -66,12 +70,6 @@ const SavedQueryName = ({ savedQueryName }: SavedQueryNameProps) => (
);
-const EuiBadgeWrap = styled(EuiBadge)`
- .euiBadge__text {
- white-space: pre-wrap !important;
- }
-`;
-
interface FiltersProps {
filters: Filter[];
dataViewId?: string;
@@ -80,48 +78,42 @@ interface FiltersProps {
}
const Filters = ({ filters, dataViewId, index, 'data-test-subj': dataTestSubj }: FiltersProps) => {
+ const flattenedFilters = mapAndFlattenFilters(filters);
+
const { indexPattern } = useRuleIndexPattern({
dataSourceType: dataViewId ? DataSourceType.DataView : DataSourceType.IndexPatterns,
index: index ?? [],
dataViewId,
});
- const flattenedFilters = mapAndFlattenFilters(filters);
+ const styles = useFiltersStyles();
return (
-
- {flattenedFilters.map((filter, idx) => (
-
-
- {indexPattern != null ? (
-
- ) : (
-
- )}
-
-
- ))}
+
+
);
};
-const QueryContent = styled.div`
- white-space: pre-wrap;
-`;
-
interface QueryProps {
query: string;
'data-test-subj'?: string;
}
-const Query = ({ query, 'data-test-subj': dataTestSubj = 'query' }: QueryProps) => (
- {query}
-);
+const Query = ({ query, 'data-test-subj': dataTestSubj = 'query' }: QueryProps) => {
+ const styles = useQueryStyles();
+ return (
+
+ {query}
+
+ );
+};
interface IndexProps {
index: string[];
@@ -257,42 +249,40 @@ const RuleType = ({ type }: RuleTypeProps) => (
{getRuleTypeDescription(type)}
);
-const StyledFieldTypeText = styled(EuiText)`
- font-size: ${({ theme }) => theme.eui.euiFontSizeXS};
- font-family: ${({ theme }) => theme.eui.euiCodeFontFamily};
- display: inline;
-`;
-
interface RequiredFieldsProps {
requiredFields: RequiredFieldArray;
}
-const RequiredFields = ({ requiredFields }: RequiredFieldsProps) => (
-
- {requiredFields.map((rF, index) => (
-
-
-
-
-
-
-
- {` ${rF.name}${index + 1 !== requiredFields.length ? ', ' : ''}`}
-
-
-
-
- ))}
-
-);
+const RequiredFields = ({ requiredFields }: RequiredFieldsProps) => {
+ const styles = useRequiredFieldsStyles();
+ return (
+
+ {requiredFields.map((rF, index) => (
+
+
+
+
+
+
+
+ {` ${rF.name}${index + 1 !== requiredFields.length ? ', ' : ''}`}
+
+
+
+
+ ))}
+
+ );
+};
interface TimelineTitleProps {
timelineTitle: string;
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/action_connectors/import_rule_action_connectors.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/action_connectors/import_rule_action_connectors.test.ts
index 8afecd245e2a9..84352c1ea0f1e 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/action_connectors/import_rule_action_connectors.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/action_connectors/import_rule_action_connectors.test.ts
@@ -13,8 +13,7 @@ import { importRuleActionConnectors } from './import_rule_action_connectors';
import { coreMock } from '@kbn/core/server/mocks';
const rules = [
- {
- ...getImportRulesSchemaMock(),
+ getImportRulesSchemaMock({
actions: [
{
group: 'default',
@@ -23,14 +22,9 @@ const rules = [
params: {},
},
],
- },
-];
-const rulesWithoutActions = [
- {
- ...getImportRulesSchemaMock(),
- actions: [],
- },
+ }),
];
+const rulesWithoutActions = [getImportRulesSchemaMock({ actions: [] })];
const actionConnectors = [webHookConnector];
const actionsClient = actionsClientMock.create();
actionsClient.getAll.mockResolvedValue([]);
@@ -115,8 +109,7 @@ describe('importRuleActionConnectors', () => {
const actionsImporter = core.savedObjects.getImporter;
const ruleWith2Connectors = [
- {
- ...getImportRulesSchemaMock(),
+ getImportRulesSchemaMock({
actions: [
{
group: 'default',
@@ -135,7 +128,7 @@ describe('importRuleActionConnectors', () => {
action_type_id: '.slack',
},
],
- },
+ }),
];
const res = await importRuleActionConnectors({
actionConnectors,
@@ -189,8 +182,7 @@ describe('importRuleActionConnectors', () => {
actionsClient,
actionsImporter: actionsImporter(),
rules: [
- {
- ...getImportRulesSchemaMock(),
+ getImportRulesSchemaMock({
actions: [
{
group: 'default',
@@ -205,7 +197,7 @@ describe('importRuleActionConnectors', () => {
params: {},
},
],
- },
+ }),
],
overwrite: false,
});
@@ -235,8 +227,8 @@ describe('importRuleActionConnectors', () => {
actionsClient,
actionsImporter: actionsImporter(),
rules: [
- {
- ...getImportRulesSchemaMock(),
+ getImportRulesSchemaMock({
+ rule_id: 'rule-1',
actions: [
{
group: 'default',
@@ -245,9 +237,9 @@ describe('importRuleActionConnectors', () => {
params: {},
},
],
- },
- {
- ...getImportRulesSchemaMock('rule-2'),
+ }),
+ getImportRulesSchemaMock({
+ rule_id: 'rule-2',
actions: [
{
group: 'default',
@@ -256,7 +248,7 @@ describe('importRuleActionConnectors', () => {
params: {},
},
],
- },
+ }),
],
overwrite: false,
});
@@ -340,45 +332,6 @@ describe('importRuleActionConnectors', () => {
expect(actionsImporter2Importer.import).not.toBeCalled();
});
- it('should not skip importing the action-connectors if all connectors have been imported/created before when overwrite is true', async () => {
- core.savedObjects.getImporter = jest.fn().mockReturnValueOnce({
- import: jest.fn().mockResolvedValue({
- success: true,
- successCount: 1,
- errors: [],
- warnings: [],
- }),
- });
- const actionsImporter = core.savedObjects.getImporter;
-
- actionsClient.getAll.mockResolvedValue([
- {
- actionTypeId: '.webhook',
- name: 'webhook',
- isPreconfigured: true,
- id: 'cabc78e0-9031-11ed-b076-53cc4d57aaf1',
- referencedByCount: 1,
- isDeprecated: false,
- isSystemAction: false,
- },
- ]);
-
- const res = await importRuleActionConnectors({
- actionConnectors,
- actionsClient,
- actionsImporter: actionsImporter(),
- rules,
- overwrite: true,
- });
-
- expect(res).toEqual({
- success: true,
- successCount: 1,
- errors: [],
- warnings: [],
- });
- });
-
it('should import one rule with connector successfully even if it was exported from different namespaces by generating destinationId and replace the old actionId with it', async () => {
const successResults = [
{
@@ -441,8 +394,8 @@ describe('importRuleActionConnectors', () => {
it('should import multiple rules with connectors successfully even if they were exported from different namespaces by generating destinationIds and replace the old actionIds with them', async () => {
const multipleRules = [
- {
- ...getImportRulesSchemaMock(),
+ getImportRulesSchemaMock({
+ rule_id: 'rule_1',
actions: [
{
group: 'default',
@@ -451,9 +404,8 @@ describe('importRuleActionConnectors', () => {
params: {},
},
],
- },
- {
- ...getImportRulesSchemaMock(),
+ }),
+ getImportRulesSchemaMock({
rule_id: 'rule_2',
id: '0abc78e0-7031-11ed-b076-53cc4d57aaf1',
actions: [
@@ -464,7 +416,7 @@ describe('importRuleActionConnectors', () => {
params: {},
},
],
- },
+ }),
];
const successResults = [
{
@@ -535,7 +487,7 @@ describe('importRuleActionConnectors', () => {
name: 'Query with a rule id',
query: 'user.name: root or user.name: admin',
risk_score: 55,
- rule_id: 'rule-1',
+ rule_id: 'rule_1',
severity: 'high',
type: 'query',
},
@@ -569,4 +521,142 @@ describe('importRuleActionConnectors', () => {
rulesWithMigratedActions,
});
});
+
+ describe('overwrite is set to "true"', () => {
+ it('should return an error when action connectors are missing in ndjson import file', async () => {
+ const rulesToImport = [
+ getImportRulesSchemaMock({
+ rule_id: 'rule-with-missed-action-connector',
+ actions: [
+ {
+ group: 'default',
+ id: 'some-connector-id',
+ params: {},
+ action_type_id: '.webhook',
+ },
+ ],
+ }),
+ ];
+
+ actionsClient.getAll.mockResolvedValue([]);
+
+ const res = await importRuleActionConnectors({
+ actionConnectors: [],
+ actionsClient,
+ actionsImporter: core.savedObjects.getImporter(),
+ rules: rulesToImport,
+ overwrite: true,
+ });
+
+ expect(res).toEqual({
+ success: false,
+ successCount: 0,
+ errors: [
+ {
+ error: {
+ message: '1 connector is missing. Connector id missing is: some-connector-id',
+ status_code: 404,
+ },
+ id: 'some-connector-id',
+ rule_id: 'rule-with-missed-action-connector',
+ },
+ ],
+ warnings: [],
+ });
+ });
+
+ it('should NOT return an error when a missing action connector in ndjson import file is a preconfigured one', async () => {
+ const rulesToImport = [
+ getImportRulesSchemaMock({
+ rule_id: 'rule-with-missed-action-connector',
+ actions: [
+ {
+ group: 'default',
+ id: 'prebuilt-connector-id',
+ params: {},
+ action_type_id: '.webhook',
+ },
+ ],
+ }),
+ ];
+
+ actionsClient.getAll.mockResolvedValue([
+ {
+ actionTypeId: '.webhook',
+ name: 'webhook',
+ isPreconfigured: true,
+ id: 'prebuilt-connector-id',
+ referencedByCount: 1,
+ isDeprecated: false,
+ isSystemAction: false,
+ },
+ ]);
+
+ const res = await importRuleActionConnectors({
+ actionConnectors: [],
+ actionsClient,
+ actionsImporter: core.savedObjects.getImporter(),
+ rules: rulesToImport,
+ overwrite: true,
+ });
+
+ expect(res).toEqual({
+ success: true,
+ successCount: 0,
+ errors: [],
+ warnings: [],
+ });
+ });
+
+ it('should not skip importing the action-connectors if all connectors have been imported/created before', async () => {
+ const rulesToImport = [
+ getImportRulesSchemaMock({
+ actions: [
+ {
+ group: 'default',
+ id: 'connector-id',
+ action_type_id: '.webhook',
+ params: {},
+ },
+ ],
+ }),
+ ];
+
+ core.savedObjects.getImporter = jest.fn().mockReturnValueOnce({
+ import: jest.fn().mockResolvedValue({
+ success: true,
+ successCount: 1,
+ errors: [],
+ warnings: [],
+ }),
+ });
+
+ actionsClient.getAll.mockResolvedValue([
+ {
+ actionTypeId: '.webhook',
+ name: 'webhook',
+ isPreconfigured: true,
+ id: 'connector-id',
+ referencedByCount: 1,
+ isDeprecated: false,
+ isSystemAction: false,
+ },
+ ]);
+
+ const res = await importRuleActionConnectors({
+ actionConnectors,
+ actionsClient,
+ actionsImporter: core.savedObjects.getImporter(),
+ rules: rulesToImport,
+ overwrite: true,
+ });
+
+ expect(res).toEqual({
+ success: true,
+ successCount: 0,
+ errors: [],
+ warnings: [],
+ });
+ });
+ });
});
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/action_connectors/import_rule_action_connectors.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/action_connectors/import_rule_action_connectors.ts
index dfbacdcfd8ce2..db86ad158289c 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/action_connectors/import_rule_action_connectors.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/action_connectors/import_rule_action_connectors.ts
@@ -8,6 +8,8 @@ import { Readable } from 'stream';
import type { SavedObjectsImportResponse } from '@kbn/core-saved-objects-common';
import type { SavedObject } from '@kbn/core-saved-objects-server';
+import type { ActionsClient } from '@kbn/actions-plugin/server';
+import type { ConnectorWithExtraFindData } from '@kbn/actions-plugin/server/application/connector/types';
import type { RuleToImport } from '../../../../../../../common/api/detection_engine/rule_management';
import type { WarningSchema } from '../../../../../../../common/api/detection_engine';
@@ -22,6 +24,13 @@ import {
} from './utils';
import type { ImportRuleActionConnectorsParams, ImportRuleActionConnectorsResult } from './types';
+const NO_ACTION_RESULT = {
+ success: true,
+ errors: [],
+ successCount: 0,
+ warnings: [],
+};
+
export const importRuleActionConnectors = async ({
actionConnectors,
actionsClient,
@@ -30,41 +39,40 @@ export const importRuleActionConnectors = async ({
overwrite,
}: ImportRuleActionConnectorsParams): Promise => {
try {
- const actionConnectorRules = getActionConnectorRules(rules);
- const actionsIds: string[] = Object.keys(actionConnectorRules);
+ const connectorIdToRuleIdsMap = getActionConnectorRules(rules);
+ const referencedConnectorIds = await filterOutPreconfiguredConnectors(
+ actionsClient,
+ Object.keys(connectorIdToRuleIdsMap)
+ );
- if (!actionsIds.length)
- return {
- success: true,
- errors: [],
- successCount: 0,
- warnings: [],
- };
+ if (!referencedConnectorIds.length) {
+ return NO_ACTION_RESULT;
+ }
- if (overwrite && !actionConnectors.length)
- return handleActionsHaveNoConnectors(actionsIds, actionConnectorRules);
+ if (overwrite && !actionConnectors.length) {
+ return handleActionsHaveNoConnectors(referencedConnectorIds, connectorIdToRuleIdsMap);
+ }
let actionConnectorsToImport: SavedObject[] = actionConnectors;
if (!overwrite) {
- const newIdsToAdd = await filterExistingActionConnectors(actionsClient, actionsIds);
+ const newIdsToAdd = await filterExistingActionConnectors(
+ actionsClient,
+ referencedConnectorIds
+ );
const foundMissingConnectors = checkIfActionsHaveMissingConnectors(
actionConnectors,
newIdsToAdd,
- actionConnectorRules
+ connectorIdToRuleIdsMap
);
if (foundMissingConnectors) return foundMissingConnectors;
// filter out existing connectors
actionConnectorsToImport = actionConnectors.filter(({ id }) => newIdsToAdd.includes(id));
}
- if (!actionConnectorsToImport.length)
- return {
- success: true,
- errors: [],
- successCount: 0,
- warnings: [],
- };
+ if (!actionConnectorsToImport.length) {
+ return NO_ACTION_RESULT;
+ }
const readStream = Readable.from(actionConnectorsToImport);
const { success, successCount, successResults, warnings, errors }: SavedObjectsImportResponse =
@@ -93,3 +101,25 @@ export const importRuleActionConnectors = async ({
return returnErroredImportResult(error);
}
};
+
+async function fetchPreconfiguredActionConnectors(
+ actionsClient: ActionsClient
+): Promise {
+ const knownConnectors = await actionsClient.getAll();
+
+ return knownConnectors.filter((c) => c.isPreconfigured);
+}
+
+async function filterOutPreconfiguredConnectors(
+ actionsClient: ActionsClient,
+ connectorsIds: string[]
+): Promise {
+ if (connectorsIds.length === 0) {
+ return [];
+ }
+
+ const preconfiguredActionConnectors = await fetchPreconfiguredActionConnectors(actionsClient);
+ const preconfiguredActionConnectorIds = new Set(preconfiguredActionConnectors.map((c) => c.id));
+
+ return connectorsIds.filter((id) => !preconfiguredActionConnectorIds.has(id));
+}
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/check_rule_exception_references.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/check_rule_exception_references.test.ts
index 2b7996c5094ab..2a249e7d9383a 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/check_rule_exception_references.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/check_rule_exception_references.test.ts
@@ -13,7 +13,7 @@ describe('checkRuleExceptionReferences', () => {
it('returns empty array if rule has no exception list references', () => {
const result = checkRuleExceptionReferences({
existingLists: {},
- rule: { ...getImportRulesSchemaMock(), exceptions_list: [] },
+ rule: getImportRulesSchemaMock({ exceptions_list: [] }),
});
expect(result).toEqual([[], []]);
@@ -29,12 +29,11 @@ describe('checkRuleExceptionReferences', () => {
type: 'detection',
},
},
- rule: {
- ...getImportRulesSchemaMock(),
+ rule: getImportRulesSchemaMock({
exceptions_list: [
{ id: '123', list_id: 'my-list', namespace_type: 'single', type: 'detection' },
],
- },
+ }),
});
expect(result).toEqual([
@@ -53,12 +52,11 @@ describe('checkRuleExceptionReferences', () => {
it('removes an exception reference if list not found to exist', () => {
const result = checkRuleExceptionReferences({
existingLists: {},
- rule: {
- ...getImportRulesSchemaMock(),
+ rule: getImportRulesSchemaMock({
exceptions_list: [
{ id: '123', list_id: 'my-list', namespace_type: 'single', type: 'detection' },
],
- },
+ }),
});
expect(result).toEqual([
@@ -86,12 +84,11 @@ describe('checkRuleExceptionReferences', () => {
type: 'detection',
},
},
- rule: {
- ...getImportRulesSchemaMock(),
+ rule: getImportRulesSchemaMock({
exceptions_list: [
{ id: '123', list_id: 'my-list', namespace_type: 'single', type: 'detection' },
],
- },
+ }),
});
expect(result).toEqual([
[
@@ -118,12 +115,11 @@ describe('checkRuleExceptionReferences', () => {
type: 'endpoint',
},
},
- rule: {
- ...getImportRulesSchemaMock(),
+ rule: getImportRulesSchemaMock({
exceptions_list: [
{ id: '123', list_id: 'my-list', namespace_type: 'single', type: 'detection' },
],
- },
+ }),
});
expect(result).toEqual([
[
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/gather_referenced_exceptions.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/gather_referenced_exceptions.test.ts
index df136fe6cfc8d..1605c745256b2 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/gather_referenced_exceptions.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/gather_referenced_exceptions.test.ts
@@ -53,12 +53,11 @@ describe('get referenced exceptions', () => {
it('returns found referenced exception lists', async () => {
const result = await getReferencedExceptionLists({
rules: [
- {
- ...getImportRulesSchemaMock(),
+ getImportRulesSchemaMock({
exceptions_list: [
{ id: '123', list_id: 'my-list', namespace_type: 'single', type: 'detection' },
],
- },
+ }),
],
savedObjectsClient,
});
@@ -77,16 +76,14 @@ describe('get referenced exceptions', () => {
it('returns found referenced exception lists when first exceptions list is empty array and second list has a value', async () => {
const result = await getReferencedExceptionLists({
rules: [
- {
- ...getImportRulesSchemaMock(),
+ getImportRulesSchemaMock({
exceptions_list: [],
- },
- {
- ...getImportRulesSchemaMock(),
+ }),
+ getImportRulesSchemaMock({
exceptions_list: [
{ id: '123', list_id: 'my-list', namespace_type: 'single', type: 'detection' },
],
- },
+ }),
],
savedObjectsClient,
});
@@ -105,18 +102,16 @@ describe('get referenced exceptions', () => {
it('returns found referenced exception lists when two rules reference same list', async () => {
const result = await getReferencedExceptionLists({
rules: [
- {
- ...getImportRulesSchemaMock(),
+ getImportRulesSchemaMock({
exceptions_list: [
{ id: '123', list_id: 'my-list', namespace_type: 'single', type: 'detection' },
],
- },
- {
- ...getImportRulesSchemaMock(),
+ }),
+ getImportRulesSchemaMock({
exceptions_list: [
{ id: '123', list_id: 'my-list', namespace_type: 'single', type: 'detection' },
],
- },
+ }),
],
savedObjectsClient,
});
@@ -157,18 +152,16 @@ describe('get referenced exceptions', () => {
const result = await getReferencedExceptionLists({
rules: [
- {
- ...getImportRulesSchemaMock(),
+ getImportRulesSchemaMock({
exceptions_list: [
{ id: '456', list_id: 'other-list', namespace_type: 'single', type: 'detection' },
],
- },
- {
- ...getImportRulesSchemaMock(),
+ }),
+ getImportRulesSchemaMock({
exceptions_list: [
{ id: '123', list_id: 'my-list', namespace_type: 'single', type: 'detection' },
],
- },
+ }),
],
savedObjectsClient,
});
@@ -207,45 +200,38 @@ describe('get referenced exceptions', () => {
describe('parseReferencdedExceptionsLists', () => {
it('should return parsed lists when exception lists are not empty', () => {
const res = parseReferencedExceptionsLists([
- {
- ...getImportRulesSchemaMock(),
+ getImportRulesSchemaMock({
exceptions_list: [
{ id: '123', list_id: 'my-list', namespace_type: 'single', type: 'detection' },
],
- },
+ }),
]);
expect(res).toEqual([[], [{ listId: 'my-list', namespaceType: 'single' }]]);
});
it('should return parsed lists when one empty exception list and one non-empty list', () => {
const res = parseReferencedExceptionsLists([
- {
- ...getImportRulesSchemaMock(),
- exceptions_list: [],
- },
- {
- ...getImportRulesSchemaMock(),
+ getImportRulesSchemaMock({ exceptions_list: [] }),
+ getImportRulesSchemaMock({
exceptions_list: [
{ id: '123', list_id: 'my-list', namespace_type: 'single', type: 'detection' },
],
- },
+ }),
]);
expect(res).toEqual([[], [{ listId: 'my-list', namespaceType: 'single' }]]);
});
it('should return parsed lists when two non-empty exception lists reference same list', () => {
const res = parseReferencedExceptionsLists([
- {
- ...getImportRulesSchemaMock(),
+ getImportRulesSchemaMock({
exceptions_list: [
{ id: '123', list_id: 'my-list', namespace_type: 'single', type: 'detection' },
],
- },
- {
- ...getImportRulesSchemaMock(),
+ }),
+ getImportRulesSchemaMock({
exceptions_list: [
{ id: '123', list_id: 'my-list', namespace_type: 'single', type: 'detection' },
],
- },
+ }),
]);
expect(res).toEqual([
[],
@@ -258,18 +244,16 @@ describe('get referenced exceptions', () => {
it('should return parsed lists when two non-empty exception lists reference differet lists', () => {
const res = parseReferencedExceptionsLists([
- {
- ...getImportRulesSchemaMock(),
+ getImportRulesSchemaMock({
exceptions_list: [
{ id: '123', list_id: 'my-list', namespace_type: 'single', type: 'detection' },
],
- },
- {
- ...getImportRulesSchemaMock(),
+ }),
+ getImportRulesSchemaMock({
exceptions_list: [
{ id: '456', list_id: 'other-list', namespace_type: 'single', type: 'detection' },
],
- },
+ }),
]);
expect(res).toEqual([
[],
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/import_rules_utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/import_rules_utils.test.ts
index 0b601be81dd62..5b097bacf2d9c 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/import_rules_utils.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/import/import_rules_utils.test.ts
@@ -74,14 +74,7 @@ describe('importRules', () => {
it('creates rule if no matching existing rule found', async () => {
const result = await importRules({
- ruleChunks: [
- [
- {
- ...getImportRulesSchemaMock(),
- rule_id: 'rule-1',
- },
- ],
- ],
+ ruleChunks: [[getImportRulesSchemaMock({ rule_id: 'rule-1' })]],
rulesResponseAcc: [],
mlAuthz,
overwriteRules: false,
@@ -98,14 +91,7 @@ describe('importRules', () => {
clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit());
const result = await importRules({
- ruleChunks: [
- [
- {
- ...getImportRulesSchemaMock(),
- rule_id: 'rule-1',
- },
- ],
- ],
+ ruleChunks: [[getImportRulesSchemaMock({ rule_id: 'rule-1' })]],
rulesResponseAcc: [],
mlAuthz,
overwriteRules: false,
@@ -129,10 +115,9 @@ describe('importRules', () => {
const result = await importRules({
ruleChunks: [
[
- {
- ...getImportRulesSchemaMock(),
+ getImportRulesSchemaMock({
rule_id: 'rule-1',
- },
+ }),
],
],
rulesResponseAcc: [],
@@ -151,14 +136,7 @@ describe('importRules', () => {
clients.rulesClient.find.mockRejectedValue(new Error('error reading rule'));
const result = await importRules({
- ruleChunks: [
- [
- {
- ...getImportRulesSchemaMock(),
- rule_id: 'rule-1',
- },
- ],
- ],
+ ruleChunks: [[getImportRulesSchemaMock({ rule_id: 'rule-1' })]],
rulesResponseAcc: [],
mlAuthz,
overwriteRules: true,
@@ -183,14 +161,7 @@ describe('importRules', () => {
(createRules as jest.Mock).mockRejectedValue(new Error('error creating rule'));
const result = await importRules({
- ruleChunks: [
- [
- {
- ...getImportRulesSchemaMock(),
- rule_id: 'rule-1',
- },
- ],
- ],
+ ruleChunks: [[getImportRulesSchemaMock({ rule_id: 'rule-1' })]],
rulesResponseAcc: [],
mlAuthz,
overwriteRules: false,
@@ -214,14 +185,7 @@ describe('importRules', () => {
clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit());
const result = await importRules({
- ruleChunks: [
- [
- {
- ...getImportRulesSchemaMock(),
- rule_id: 'rule-1',
- },
- ],
- ],
+ ruleChunks: [[getImportRulesSchemaMock({ rule_id: 'rule-1' })]],
rulesResponseAcc: [],
mlAuthz,
overwriteRules: true,
diff --git a/x-pack/plugins/stack_alerts/server/rule_types/es_query/lib/fetch_search_source_query.test.ts b/x-pack/plugins/stack_alerts/server/rule_types/es_query/lib/fetch_search_source_query.test.ts
index a753c34908a20..ca5a2736408d4 100644
--- a/x-pack/plugins/stack_alerts/server/rule_types/es_query/lib/fetch_search_source_query.test.ts
+++ b/x-pack/plugins/stack_alerts/server/rule_types/es_query/lib/fetch_search_source_query.test.ts
@@ -7,14 +7,22 @@
import { OnlySearchSourceRuleParams } from '../types';
import { createSearchSourceMock } from '@kbn/data-plugin/common/search/search_source/mocks';
-import { updateSearchSource, getSmallerDataViewSpec } from './fetch_search_source_query';
+import {
+ updateSearchSource,
+ generateLink,
+ updateFilterReferences,
+ getSmallerDataViewSpec,
+} from './fetch_search_source_query';
import {
createStubDataView,
stubbedSavedObjectIndexPattern,
} from '@kbn/data-views-plugin/common/data_view.stub';
-import { DataView } from '@kbn/data-views-plugin/common';
+import { DataView, DataViewSpec } from '@kbn/data-views-plugin/common';
import { fieldFormatsMock } from '@kbn/field-formats-plugin/common/mocks';
import { Comparator } from '../../../../common/comparator_types';
+import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks';
+import { DiscoverAppLocatorParams } from '@kbn/discover-plugin/common';
+import { LocatorPublic } from '@kbn/share-plugin/common';
const createDataView = () => {
const id = 'test-id';
@@ -55,26 +63,27 @@ const defaultParams: OnlySearchSourceRuleParams = {
};
describe('fetchSearchSourceQuery', () => {
- describe('updateSearchSource', () => {
- const dataViewMock = createDataView();
- afterAll(() => {
- jest.resetAllMocks();
- });
+ const dataViewMock = createDataView();
- const fakeNow = new Date('2020-02-09T23:15:41.941Z');
+ afterAll(() => {
+ jest.resetAllMocks();
+ });
- beforeAll(() => {
- jest.resetAllMocks();
- global.Date.now = jest.fn(() => fakeNow.getTime());
- });
+ const fakeNow = new Date('2020-02-09T23:15:41.941Z');
+
+ beforeAll(() => {
+ jest.resetAllMocks();
+ global.Date.now = jest.fn(() => fakeNow.getTime());
+ });
+ describe('updateSearchSource', () => {
it('without latest timestamp', async () => {
const params = { ...defaultParams, thresholdComparator: Comparator.GT_OR_EQ, threshold: [3] };
const searchSourceInstance = createSearchSourceMock({ index: dataViewMock });
const { dateStart, dateEnd } = getTimeRange();
- const searchSource = updateSearchSource(
+ const { searchSource, filterToExcludeHitsFromPreviousRun } = updateSearchSource(
searchSourceInstance,
dataViewMock,
params,
@@ -83,6 +92,7 @@ describe('fetchSearchSourceQuery', () => {
dateEnd
);
const searchRequest = searchSource.getSearchRequestBody();
+ expect(filterToExcludeHitsFromPreviousRun).toBe(null);
expect(searchRequest.size).toMatchInlineSnapshot(`100`);
expect(searchRequest.query).toMatchInlineSnapshot(`
Object {
@@ -113,7 +123,7 @@ describe('fetchSearchSourceQuery', () => {
const searchSourceInstance = createSearchSourceMock({ index: dataViewMock });
const { dateStart, dateEnd } = getTimeRange();
- const searchSource = updateSearchSource(
+ const { searchSource, filterToExcludeHitsFromPreviousRun } = updateSearchSource(
searchSourceInstance,
dataViewMock,
params,
@@ -122,6 +132,23 @@ describe('fetchSearchSourceQuery', () => {
dateEnd
);
const searchRequest = searchSource.getSearchRequestBody();
+ expect(filterToExcludeHitsFromPreviousRun).toMatchInlineSnapshot(`
+ Object {
+ "meta": Object {
+ "field": "time",
+ "index": "test-id",
+ "params": Object {},
+ },
+ "query": Object {
+ "range": Object {
+ "time": Object {
+ "format": "strict_date_optional_time",
+ "gt": "2020-02-09T23:12:41.941Z",
+ },
+ },
+ },
+ }
+ `);
expect(searchRequest.size).toMatchInlineSnapshot(`100`);
expect(searchRequest.query).toMatchInlineSnapshot(`
Object {
@@ -160,7 +187,7 @@ describe('fetchSearchSourceQuery', () => {
const searchSourceInstance = createSearchSourceMock({ index: dataViewMock });
const { dateStart, dateEnd } = getTimeRange();
- const searchSource = updateSearchSource(
+ const { searchSource, filterToExcludeHitsFromPreviousRun } = updateSearchSource(
searchSourceInstance,
dataViewMock,
params,
@@ -169,6 +196,7 @@ describe('fetchSearchSourceQuery', () => {
dateEnd
);
const searchRequest = searchSource.getSearchRequestBody();
+ expect(filterToExcludeHitsFromPreviousRun).toBe(null);
expect(searchRequest.size).toMatchInlineSnapshot(`100`);
expect(searchRequest.query).toMatchInlineSnapshot(`
Object {
@@ -199,7 +227,7 @@ describe('fetchSearchSourceQuery', () => {
const searchSourceInstance = createSearchSourceMock({ index: dataViewMock });
const { dateStart, dateEnd } = getTimeRange();
- const searchSource = updateSearchSource(
+ const { searchSource, filterToExcludeHitsFromPreviousRun } = updateSearchSource(
searchSourceInstance,
dataViewMock,
params,
@@ -208,6 +236,7 @@ describe('fetchSearchSourceQuery', () => {
dateEnd
);
const searchRequest = searchSource.getSearchRequestBody();
+ expect(filterToExcludeHitsFromPreviousRun).toBe(null);
expect(searchRequest.size).toMatchInlineSnapshot(`100`);
expect(searchRequest.query).toMatchInlineSnapshot(`
Object {
@@ -244,7 +273,7 @@ describe('fetchSearchSourceQuery', () => {
const searchSourceInstance = createSearchSourceMock({ index: dataViewMock });
const { dateStart, dateEnd } = getTimeRange();
- const searchSource = updateSearchSource(
+ const { searchSource } = updateSearchSource(
searchSourceInstance,
dataViewMock,
params,
@@ -307,6 +336,95 @@ describe('fetchSearchSourceQuery', () => {
});
});
+ describe('generateLink', () => {
+ it('should include additional time filter', async () => {
+ const params = { ...defaultParams, thresholdComparator: Comparator.GT_OR_EQ, threshold: [3] };
+
+ const searchSourceInstance = createSearchSourceMock({ index: dataViewMock });
+
+ const { dateStart, dateEnd } = getTimeRange();
+ const { filterToExcludeHitsFromPreviousRun } = updateSearchSource(
+ searchSourceInstance,
+ dataViewMock,
+ params,
+ '2020-02-09T23:12:41.941Z',
+ dateStart,
+ dateEnd
+ );
+
+ expect(filterToExcludeHitsFromPreviousRun).toMatchInlineSnapshot(`
+ Object {
+ "meta": Object {
+ "field": "time",
+ "index": "test-id",
+ "params": Object {},
+ },
+ "query": Object {
+ "range": Object {
+ "time": Object {
+ "format": "strict_date_optional_time",
+ "gt": "2020-02-09T23:12:41.941Z",
+ },
+ },
+ },
+ }
+ `);
+
+ const locatorMock = {
+ getRedirectUrl: jest.fn(() => '/app/r?l=DISCOVER_APP_LOCATOR'),
+ } as unknown as LocatorPublic;
+
+ const dataViews = {
+ ...dataViewPluginMocks.createStartContract(),
+ create: async (spec: DataViewSpec) =>
+ new DataView({ spec, fieldFormats: fieldFormatsMock }),
+ };
+
+ const linkWithoutExcludedRuns = await generateLink(
+ searchSourceInstance,
+ locatorMock,
+ dataViews,
+ dataViewMock,
+ dateStart,
+ dateEnd,
+ 'test1',
+ null
+ );
+
+ expect(linkWithoutExcludedRuns).toBe('test1/app/r?l=DISCOVER_APP_LOCATOR');
+ expect(locatorMock.getRedirectUrl).toHaveBeenCalledWith(
+ expect.objectContaining({
+ filters: [],
+ })
+ );
+
+ const linkWithExcludedRuns = await generateLink(
+ searchSourceInstance,
+ locatorMock,
+ dataViews,
+ dataViewMock,
+ dateStart,
+ dateEnd,
+ 'test2',
+ filterToExcludeHitsFromPreviousRun
+ );
+
+ expect(linkWithExcludedRuns).toBe('test2/app/r?l=DISCOVER_APP_LOCATOR');
+ expect(locatorMock.getRedirectUrl).toHaveBeenNthCalledWith(
+ 2,
+ expect.objectContaining({
+ filters: expect.arrayContaining(
+ updateFilterReferences(
+ [filterToExcludeHitsFromPreviousRun!],
+ dataViewMock.id!,
+ undefined
+ )
+ ),
+ })
+ );
+ });
+ });
+
describe('getSmallerDataViewSpec', () => {
it('should remove "count"s but keep other props like "customLabel"', async () => {
const fieldsMap = {
diff --git a/x-pack/plugins/stack_alerts/server/rule_types/es_query/lib/fetch_search_source_query.ts b/x-pack/plugins/stack_alerts/server/rule_types/es_query/lib/fetch_search_source_query.ts
index 455ce4c7dc9bc..84c0dbf6ef5f2 100644
--- a/x-pack/plugins/stack_alerts/server/rule_types/es_query/lib/fetch_search_source_query.ts
+++ b/x-pack/plugins/stack_alerts/server/rule_types/es_query/lib/fetch_search_source_query.ts
@@ -60,7 +60,7 @@ export async function fetchSearchSourceQuery({
const initialSearchSource = await searchSourceClient.create(params.searchConfiguration);
const index = initialSearchSource.getField('index') as DataView;
- const searchSource = updateSearchSource(
+ const { searchSource, filterToExcludeHitsFromPreviousRun } = updateSearchSource(
initialSearchSource,
index,
params,
@@ -85,7 +85,8 @@ export async function fetchSearchSourceQuery({
index,
dateStart,
dateEnd,
- spacePrefix
+ spacePrefix,
+ filterToExcludeHitsFromPreviousRun
);
return {
link,
@@ -104,7 +105,7 @@ export function updateSearchSource(
dateStart: string,
dateEnd: string,
alertLimit?: number
-) {
+): { searchSource: ISearchSource; filterToExcludeHitsFromPreviousRun: Filter | null } {
const isGroupAgg = isGroupAggregation(params.termField);
const timeFieldName = params.timeField || index.timeFieldName;
@@ -123,16 +124,17 @@ export function updateSearchSource(
),
];
+ let filterToExcludeHitsFromPreviousRun = null;
if (params.excludeHitsFromPreviousRun) {
if (latestTimestamp && latestTimestamp > dateStart) {
- // add additional filter for documents with a timestamp greater then
+ // add additional filter for documents with a timestamp greater than
// the timestamp of the previous run, so that those documents are not counted twice
- const addTimeRangeField = buildRangeFilter(
+ filterToExcludeHitsFromPreviousRun = buildRangeFilter(
field!,
{ gt: latestTimestamp, format: 'strict_date_optional_time' },
index
);
- filters.push(addTimeRangeField);
+ filters.push(filterToExcludeHitsFromPreviousRun);
}
}
@@ -164,19 +166,31 @@ export function updateSearchSource(
...(isGroupAgg ? { topHitsSize: params.size } : {}),
})
);
- return searchSourceChild;
+ return {
+ searchSource: searchSourceChild,
+ filterToExcludeHitsFromPreviousRun,
+ };
}
-async function generateLink(
+export async function generateLink(
searchSource: ISearchSource,
discoverLocator: LocatorPublic,
dataViews: DataViewsContract,
dataViewToUpdate: DataView,
dateStart: string,
dateEnd: string,
- spacePrefix: string
+ spacePrefix: string,
+ filterToExcludeHitsFromPreviousRun: Filter | null
) {
- const prevFilters = searchSource.getField('filter') as Filter[];
+ const prevFilters = [...((searchSource.getField('filter') as Filter[]) || [])];
+
+ if (filterToExcludeHitsFromPreviousRun) {
+ // Using the same additional filter as in the alert check above.
+ // We cannot simply pass `latestTimestamp` to `timeRange.from` Discover locator params
+ // as that would include `latestTimestamp` itself in the Discover results which would be wrong.
+ // Results should be after `latestTimestamp` and within `dateStart` and `dateEnd`.
+ prevFilters.push(filterToExcludeHitsFromPreviousRun);
+ }
// make new adhoc data view
const newDataView = await dataViews.create({
@@ -202,7 +216,11 @@ async function generateLink(
return start + spacePrefix + '/app' + end;
}
-function updateFilterReferences(filters: Filter[], fromDataView: string, toDataView: string) {
+export function updateFilterReferences(
+ filters: Filter[],
+ fromDataView: string,
+ toDataView: string | undefined
+) {
return (filters || []).map((filter) => {
if (filter.meta.index === fromDataView) {
return {
diff --git a/x-pack/plugins/synthetics/server/routes/create_route_with_auth.test.ts b/x-pack/plugins/synthetics/server/routes/create_route_with_auth.test.ts
index 36262e2402b8e..30b69e0b95e4b 100644
--- a/x-pack/plugins/synthetics/server/routes/create_route_with_auth.test.ts
+++ b/x-pack/plugins/synthetics/server/routes/create_route_with_auth.test.ts
@@ -6,9 +6,37 @@
*/
import { createSyntheticsRouteWithAuth } from './create_route_with_auth';
+import { SupportedMethod } from './types';
+
+const methods: SupportedMethod[][] = [['GET'], ['POST'], ['PUT'], ['DELETE']];
describe('createSyntheticsRouteWithAuth', () => {
- it('should create a route with auth', () => {
+ it.each(
+ methods
+ .map<[SupportedMethod, boolean]>((m) => [m[0], true])
+ .concat(methods.map((m) => [m[0], false]))
+ )('%s methods continues to support the writeAccess %s flag', (mStr, writeAccess) => {
+ const method: SupportedMethod = mStr as SupportedMethod;
+ const route = createSyntheticsRouteWithAuth(() => ({
+ method,
+ path: '/foo',
+ validate: {},
+ writeAccess,
+ handler: async () => {
+ return { success: true };
+ },
+ }));
+
+ expect(route).toEqual({
+ method,
+ path: '/foo',
+ validate: {},
+ handler: expect.any(Function),
+ writeAccess,
+ });
+ });
+
+ it('by default allows read access for GET by default', () => {
const route = createSyntheticsRouteWithAuth(() => ({
method: 'GET',
path: '/foo',
@@ -27,11 +55,9 @@ describe('createSyntheticsRouteWithAuth', () => {
});
});
- it.each([['POST'], ['PUT'], ['DELETE']])(
- 'requires write permissions for %s requests',
+ it.each(methods.filter((m) => m[0] !== 'GET'))(
+ 'by default requires write access for %s route requests',
(method) => {
- if (method !== 'POST' && method !== 'PUT' && method !== 'DELETE')
- throw Error('Invalid method');
const route = createSyntheticsRouteWithAuth(() => ({
method,
path: '/foo',
@@ -50,29 +76,4 @@ describe('createSyntheticsRouteWithAuth', () => {
});
}
);
-
- it.each([['POST'], ['PUT'], ['DELETE']])(
- 'allows write access override for %s requests',
- (method) => {
- if (method !== 'POST' && method !== 'PUT' && method !== 'DELETE')
- throw Error('Invalid method');
- const route = createSyntheticsRouteWithAuth(() => ({
- method,
- path: '/foo',
- validate: {},
- handler: async () => {
- return { success: true };
- },
- writeAccessOverride: true,
- }));
-
- expect(route).toEqual({
- method,
- path: '/foo',
- validate: {},
- handler: expect.any(Function),
- writeAccess: undefined,
- });
- }
- );
});
diff --git a/x-pack/plugins/synthetics/server/routes/create_route_with_auth.ts b/x-pack/plugins/synthetics/server/routes/create_route_with_auth.ts
index f4fb413011614..d9b6f672eb3e0 100644
--- a/x-pack/plugins/synthetics/server/routes/create_route_with_auth.ts
+++ b/x-pack/plugins/synthetics/server/routes/create_route_with_auth.ts
@@ -11,20 +11,23 @@ import {
LICENSE_NOT_ACTIVE_ERROR,
LICENSE_NOT_SUPPORTED_ERROR,
} from '../../common/constants';
-import { SyntheticsRestApiRouteFactory, SyntheticsRoute, SyntheticsRouteHandler } from './types';
+import {
+ SupportedMethod,
+ SyntheticsRestApiRouteFactory,
+ SyntheticsRoute,
+ SyntheticsRouteHandler,
+} from './types';
-function getWriteAccessFlag(method: string, writeAccessOverride?: boolean, writeAccess?: boolean) {
- // if route includes an override, skip write-only access with `undefined`
- // otherwise, if route is not a GET, require write access
- // if route is get, use writeAccess value with `false` as default
- return writeAccessOverride === true ? undefined : method !== 'GET' ? true : writeAccess ?? false;
+function getDefaultWriteAccessFlag(method: SupportedMethod) {
+ // if the method is not GET, it defaults to requiring write access
+ return method !== 'GET';
}
export const createSyntheticsRouteWithAuth = (
routeCreator: SyntheticsRestApiRouteFactory
): SyntheticsRoute => {
const restRoute = routeCreator();
- const { handler, method, path, options, writeAccess, writeAccessOverride, ...rest } = restRoute;
+ const { handler, method, path, options, writeAccess, ...rest } = restRoute;
const licenseCheckHandler: SyntheticsRouteHandler = async ({
context,
response,
@@ -56,7 +59,7 @@ export const createSyntheticsRouteWithAuth = (
options,
handler: licenseCheckHandler,
...rest,
- writeAccess: getWriteAccessFlag(method, writeAccessOverride, writeAccess),
+ writeAccess: writeAccess ?? getDefaultWriteAccessFlag(method),
};
};
diff --git a/x-pack/plugins/synthetics/server/routes/pings/journey_screenshot_blocks.ts b/x-pack/plugins/synthetics/server/routes/pings/journey_screenshot_blocks.ts
index f75a92ea555d8..8d6c62d20b7fc 100644
--- a/x-pack/plugins/synthetics/server/routes/pings/journey_screenshot_blocks.ts
+++ b/x-pack/plugins/synthetics/server/routes/pings/journey_screenshot_blocks.ts
@@ -22,7 +22,7 @@ export const createJourneyScreenshotBlocksRoute: SyntheticsRestApiRouteFactory =
hashes: schema.arrayOf(schema.string()),
}),
},
- writeAccessOverride: true,
+ writeAccess: false,
handler: async (routeProps) => {
return await journeyScreenshotBlocksHandler(routeProps);
},
diff --git a/x-pack/plugins/synthetics/server/routes/synthetics_service/enablement.ts b/x-pack/plugins/synthetics/server/routes/synthetics_service/enablement.ts
index 5124df7af6ce0..4c29cb300d6fa 100644
--- a/x-pack/plugins/synthetics/server/routes/synthetics_service/enablement.ts
+++ b/x-pack/plugins/synthetics/server/routes/synthetics_service/enablement.ts
@@ -16,7 +16,7 @@ import {
export const getSyntheticsEnablementRoute: SyntheticsRestApiRouteFactory = () => ({
method: 'PUT',
path: SYNTHETICS_API_URLS.SYNTHETICS_ENABLEMENT,
- writeAccessOverride: true,
+ writeAccess: false,
validate: {},
handler: async ({ savedObjectsClient, request, server }): Promise => {
try {
diff --git a/x-pack/plugins/synthetics/server/routes/synthetics_service/test_now_monitor.ts b/x-pack/plugins/synthetics/server/routes/synthetics_service/test_now_monitor.ts
index 08c4f8dbebb3c..c2569153ce94d 100644
--- a/x-pack/plugins/synthetics/server/routes/synthetics_service/test_now_monitor.ts
+++ b/x-pack/plugins/synthetics/server/routes/synthetics_service/test_now_monitor.ts
@@ -27,6 +27,7 @@ export const testNowMonitorRoute: SyntheticsRestApiRouteFactory
const { monitorId } = routeContext.request.params;
return triggerTestNow(monitorId, routeContext);
},
+ writeAccess: true,
});
export const triggerTestNow = async (
diff --git a/x-pack/plugins/synthetics/server/routes/types.ts b/x-pack/plugins/synthetics/server/routes/types.ts
index 4d27206a16f0e..aa3f161675ae8 100644
--- a/x-pack/plugins/synthetics/server/routes/types.ts
+++ b/x-pack/plugins/synthetics/server/routes/types.ts
@@ -26,13 +26,14 @@ export type SyntheticsRequest = KibanaRequest<
Record
>;
+export type SupportedMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
+
/**
* Defines the basic properties employed by Uptime routes.
*/
export interface UMServerRoute {
- method: 'GET' | 'PUT' | 'POST' | 'DELETE';
+ method: SupportedMethod;
writeAccess?: boolean;
- writeAccessOverride?: boolean;
handler: T;
validation?: FullValidationConfig;
streamHandler?: (
diff --git a/x-pack/plugins/timelines/common/api/search_strategy/timeline/eql.test.ts b/x-pack/plugins/timelines/common/api/search_strategy/timeline/eql.test.ts
new file mode 100644
index 0000000000000..914bb685cd5f2
--- /dev/null
+++ b/x-pack/plugins/timelines/common/api/search_strategy/timeline/eql.test.ts
@@ -0,0 +1,109 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { timelineEqlRequestOptionsSchema } from './eql';
+import { mockBaseTimelineRequest } from './mocks/base_timeline_request';
+
+const mockEqlRequestOptions = {
+ ...mockBaseTimelineRequest,
+ filterQuery: 'sequence\n[any where true]\n[any where true]',
+ eventCategoryField: 'event.category',
+ tiebreakerField: '',
+ fieldRequested: [
+ '@timestamp',
+ 'message',
+ 'event.category',
+ 'event.action',
+ 'host.name',
+ 'source.ip',
+ 'destination.ip',
+ 'user.name',
+ '@timestamp',
+ 'kibana.alert.workflow_status',
+ 'kibana.alert.workflow_tags',
+ 'kibana.alert.workflow_assignee_ids',
+ 'kibana.alert.group.id',
+ 'kibana.alert.original_time',
+ 'kibana.alert.building_block_type',
+ 'kibana.alert.rule.from',
+ 'kibana.alert.rule.name',
+ 'kibana.alert.rule.to',
+ 'kibana.alert.rule.uuid',
+ 'kibana.alert.rule.rule_id',
+ 'kibana.alert.rule.type',
+ 'kibana.alert.suppression.docs_count',
+ 'kibana.alert.original_event.kind',
+ 'kibana.alert.original_event.module',
+ 'file.path',
+ 'file.Ext.code_signature.subject_name',
+ 'file.Ext.code_signature.trusted',
+ 'file.hash.sha256',
+ 'host.os.family',
+ 'event.code',
+ 'process.entry_leader.entity_id',
+ ],
+ language: 'eql',
+ pagination: {
+ activePage: 0,
+ querySize: 25,
+ },
+ runtimeMappings: {},
+ size: 100,
+ sort: [
+ {
+ direction: 'asc',
+ esTypes: ['date'],
+ field: '@timestamp',
+ type: 'date',
+ },
+ ],
+ timerange: {
+ from: '2018-02-12T20:39:22.229Z',
+ interval: '12h',
+ to: '2024-02-13T20:39:22.229Z',
+ },
+ timestampField: '@timestamp',
+};
+
+describe('timelineEqlRequestOptionsSchema', () => {
+ it('should correctly parse the last eql request object without unknown fields', () => {
+ expect(timelineEqlRequestOptionsSchema.parse(mockEqlRequestOptions)).toEqual(
+ mockEqlRequestOptions
+ );
+ });
+
+ it('should correctly parse the last eql request object and remove unknown fields', () => {
+ const invalidEqlRequest = {
+ ...mockEqlRequestOptions,
+ unknownField: 'should-be-removed',
+ };
+ expect(timelineEqlRequestOptionsSchema.parse(invalidEqlRequest)).toEqual(mockEqlRequestOptions);
+ });
+
+ it('should correctly error if an incorrect field type is provided for a schema key', () => {
+ const invalidEqlRequest = {
+ ...mockEqlRequestOptions,
+ fieldRequested: 123,
+ };
+
+ expect(() => {
+ timelineEqlRequestOptionsSchema.parse(invalidEqlRequest);
+ }).toThrowErrorMatchingInlineSnapshot(`
+ "[
+ {
+ \\"code\\": \\"invalid_type\\",
+ \\"expected\\": \\"array\\",
+ \\"received\\": \\"number\\",
+ \\"path\\": [
+ \\"fieldRequested\\"
+ ],
+ \\"message\\": \\"Expected array, received number\\"
+ }
+ ]"
+ `);
+ });
+});
diff --git a/x-pack/plugins/timelines/common/api/search_strategy/timeline/events_all.test.ts b/x-pack/plugins/timelines/common/api/search_strategy/timeline/events_all.test.ts
new file mode 100644
index 0000000000000..1c69e529c66f7
--- /dev/null
+++ b/x-pack/plugins/timelines/common/api/search_strategy/timeline/events_all.test.ts
@@ -0,0 +1,76 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { timelineEventsAllSchema } from './events_all';
+import { mockBaseTimelineRequest } from './mocks/base_timeline_request';
+
+const mockEventsAllRequest = {
+ ...mockBaseTimelineRequest,
+ factoryQueryType: 'eventsAll',
+ excludeEcsData: false,
+ pagination: { activePage: 0, querySize: 25 },
+ fieldRequested: [
+ '@timestamp',
+ '_index',
+ 'message',
+ 'host.name',
+ 'event.module',
+ 'agent.type',
+ 'event.dataset',
+ 'event.action',
+ 'user.name',
+ 'source.ip',
+ 'destination.ip',
+ ],
+ sort: [
+ {
+ field: '@timestamp',
+ type: 'date',
+ direction: 'desc',
+ esTypes: [],
+ },
+ ],
+ fields: [],
+ language: 'kuery',
+};
+
+describe('timelineEventsAllSchema', () => {
+ it('should correctly parse the events request object', () => {
+ expect(timelineEventsAllSchema.parse(mockEventsAllRequest)).toEqual(mockEventsAllRequest);
+ });
+
+ it('should correctly parse the events request object and remove unknown fields', () => {
+ const invalidEventsRequest = {
+ ...mockEventsAllRequest,
+ unknownField: 'shouldBeRemoved',
+ };
+ expect(timelineEventsAllSchema.parse(invalidEventsRequest)).toEqual(mockEventsAllRequest);
+ });
+
+ it('should correctly error if an incorrect field type is provided for a schema key', () => {
+ const invalidEventsRequest = {
+ ...mockEventsAllRequest,
+ excludeEcsData: 'notABoolean',
+ };
+
+ expect(() => {
+ timelineEventsAllSchema.parse(invalidEventsRequest);
+ }).toThrowErrorMatchingInlineSnapshot(`
+ "[
+ {
+ \\"code\\": \\"invalid_type\\",
+ \\"expected\\": \\"boolean\\",
+ \\"received\\": \\"string\\",
+ \\"path\\": [
+ \\"excludeEcsData\\"
+ ],
+ \\"message\\": \\"Expected boolean, received string\\"
+ }
+ ]"
+ `);
+ });
+});
diff --git a/x-pack/plugins/timelines/common/api/search_strategy/timeline/events_details.test.ts b/x-pack/plugins/timelines/common/api/search_strategy/timeline/events_details.test.ts
new file mode 100644
index 0000000000000..57adc28cf8434
--- /dev/null
+++ b/x-pack/plugins/timelines/common/api/search_strategy/timeline/events_details.test.ts
@@ -0,0 +1,55 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { timelineEventsDetailsSchema } from './events_details';
+
+const mockEventsDetails = {
+ entityType: 'events',
+ indexName: 'test-large-index',
+ eventId: 'enfXnY0Byt9Ce9tO1aWh',
+ factoryQueryType: 'eventsDetails',
+ runtimeMappings: {},
+};
+
+describe('timelineEventsDetailsSchema', () => {
+ it('should correctly parse the event details request schema', () => {
+ expect(timelineEventsDetailsSchema.parse(mockEventsDetails)).toEqual(mockEventsDetails);
+ });
+
+ it('should correctly parse the event details request schema and remove unknown fields', () => {
+ const invalidEventsDetailsRequest = {
+ ...mockEventsDetails,
+ unknownField: 'should-be-removed',
+ };
+ expect(timelineEventsDetailsSchema.parse(invalidEventsDetailsRequest)).toEqual(
+ mockEventsDetails
+ );
+ });
+
+ it('should correctly error if an incorrect field type is provided for a schema key', () => {
+ const invalidEventsDetailsRequest = {
+ ...mockEventsDetails,
+ indexName: 123,
+ };
+
+ expect(() => {
+ timelineEventsDetailsSchema.parse(invalidEventsDetailsRequest);
+ }).toThrowErrorMatchingInlineSnapshot(`
+ "[
+ {
+ \\"code\\": \\"invalid_type\\",
+ \\"expected\\": \\"string\\",
+ \\"received\\": \\"number\\",
+ \\"path\\": [
+ \\"indexName\\"
+ ],
+ \\"message\\": \\"Expected string, received number\\"
+ }
+ ]"
+ `);
+ });
+});
diff --git a/x-pack/plugins/timelines/common/api/search_strategy/timeline/events_last_event_time.test.ts b/x-pack/plugins/timelines/common/api/search_strategy/timeline/events_last_event_time.test.ts
new file mode 100644
index 0000000000000..1ccacb265416e
--- /dev/null
+++ b/x-pack/plugins/timelines/common/api/search_strategy/timeline/events_last_event_time.test.ts
@@ -0,0 +1,69 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { timelineEventsLastEventTimeRequestSchema } from './events_last_event_time';
+import { mockBaseTimelineRequest } from './mocks/base_timeline_request';
+
+const mockEventsLastEventTimeRequest = {
+ ...mockBaseTimelineRequest,
+ // Remove fields that are omitted in the schema
+ runtimeMappings: undefined,
+ filterQuery: undefined,
+ timerange: undefined,
+ // Add eventsLastEventTime specific fields
+ factoryQueryType: 'eventsLastEventTime',
+ indexKey: 'hosts',
+ details: {},
+};
+
+describe('timelineEventsLastEventTimeRequestSchema', () => {
+ it('should correctly parse the last event time request object without unknown fields', () => {
+ expect(timelineEventsLastEventTimeRequestSchema.parse(mockEventsLastEventTimeRequest)).toEqual(
+ mockEventsLastEventTimeRequest
+ );
+ });
+
+ it('should correctly parse the last event time request object and remove unknown fields', () => {
+ const invalidEventsDetailsRequest = {
+ ...mockEventsLastEventTimeRequest,
+ unknownField: 'should-be-removed',
+ };
+ expect(timelineEventsLastEventTimeRequestSchema.parse(invalidEventsDetailsRequest)).toEqual(
+ mockEventsLastEventTimeRequest
+ );
+ });
+
+ it('should correctly error if an incorrect field type is provided for a schema key', () => {
+ const invalidEventsDetailsRequest = {
+ ...mockEventsLastEventTimeRequest,
+ indexKey: 'unknown-key',
+ };
+
+ expect(() => {
+ timelineEventsLastEventTimeRequestSchema.parse(invalidEventsDetailsRequest);
+ }).toThrowErrorMatchingInlineSnapshot(`
+ "[
+ {
+ \\"received\\": \\"unknown-key\\",
+ \\"code\\": \\"invalid_enum_value\\",
+ \\"options\\": [
+ \\"hostDetails\\",
+ \\"hosts\\",
+ \\"users\\",
+ \\"userDetails\\",
+ \\"ipDetails\\",
+ \\"network\\"
+ ],
+ \\"path\\": [
+ \\"indexKey\\"
+ ],
+ \\"message\\": \\"Invalid enum value. Expected 'hostDetails' | 'hosts' | 'users' | 'userDetails' | 'ipDetails' | 'network', received 'unknown-key'\\"
+ }
+ ]"
+ `);
+ });
+});
diff --git a/x-pack/plugins/timelines/common/api/search_strategy/timeline/kpi.test.ts b/x-pack/plugins/timelines/common/api/search_strategy/timeline/kpi.test.ts
new file mode 100644
index 0000000000000..ade3b954e9210
--- /dev/null
+++ b/x-pack/plugins/timelines/common/api/search_strategy/timeline/kpi.test.ts
@@ -0,0 +1,51 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { timelineKpiRequestOptionsSchema } from './kpi';
+import { mockBaseTimelineRequest } from './mocks/base_timeline_request';
+
+const mockKpiRequest = {
+ ...mockBaseTimelineRequest,
+ factoryQueryType: 'eventsKpi',
+};
+
+describe('timelineKpiRequestOptionsSchema', () => {
+ it('should correctly parse the events kpi request object', () => {
+ expect(timelineKpiRequestOptionsSchema.parse(mockKpiRequest)).toEqual(mockKpiRequest);
+ });
+
+ it('should correctly parse the events kpi request object and remove unknown fields', () => {
+ const invalidKpiRequest = {
+ ...mockKpiRequest,
+ unknownField: 'shouldBeRemoved',
+ };
+ expect(timelineKpiRequestOptionsSchema.parse(invalidKpiRequest)).toEqual(mockKpiRequest);
+ });
+
+ it('should correctly error if an incorrect field type is provided for a schema key', () => {
+ const invalidKpiRequest = {
+ ...mockKpiRequest,
+ factoryQueryType: 'someOtherType',
+ };
+
+ expect(() => {
+ timelineKpiRequestOptionsSchema.parse(invalidKpiRequest);
+ }).toThrowErrorMatchingInlineSnapshot(`
+ "[
+ {
+ \\"received\\": \\"someOtherType\\",
+ \\"code\\": \\"invalid_literal\\",
+ \\"expected\\": \\"eventsKpi\\",
+ \\"path\\": [
+ \\"factoryQueryType\\"
+ ],
+ \\"message\\": \\"Invalid literal value, expected \\\\\\"eventsKpi\\\\\\"\\"
+ }
+ ]"
+ `);
+ });
+});
diff --git a/x-pack/plugins/timelines/common/api/search_strategy/timeline/mocks/base_timeline_request.ts b/x-pack/plugins/timelines/common/api/search_strategy/timeline/mocks/base_timeline_request.ts
new file mode 100644
index 0000000000000..c1ab5fc35b641
--- /dev/null
+++ b/x-pack/plugins/timelines/common/api/search_strategy/timeline/mocks/base_timeline_request.ts
@@ -0,0 +1,20 @@
+/*
+ * 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.
+ */
+
+export const mockBaseTimelineRequest = {
+ id: 'Fnh1dVQ4SDRTUldtRXpUcDEwZXliWHcdZXdlWVBFWkVSWHVIdzY4a19JbFRvUTozMzgzNzk=',
+ defaultIndex: ['*-large-index'],
+ filterQuery:
+ '{"bool":{"must":[],"filter":[{"bool":{"should":[{"exists":{"field":"host.name"}}],"minimum_should_match":1}},{"range":{"@timestamp":{"gte":"2019-02-13T15:39:10.392Z","lt":"2024-02-14T04:59:59.999Z","format":"strict_date_optional_time"}}}],"should":[],"must_not":[]}}',
+ runtimeMappings: {},
+ timerange: {
+ interval: '12h',
+ from: '2019-02-13T15:39:10.392Z',
+ to: '2024-02-14T04:59:59.999Z',
+ },
+ entityType: 'events',
+};
diff --git a/x-pack/plugins/timelines/common/api/search_strategy/timeline/request_basic.test.ts b/x-pack/plugins/timelines/common/api/search_strategy/timeline/request_basic.test.ts
new file mode 100644
index 0000000000000..2a6b4c59f95e6
--- /dev/null
+++ b/x-pack/plugins/timelines/common/api/search_strategy/timeline/request_basic.test.ts
@@ -0,0 +1,53 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { timelineRequestBasicOptionsSchema } from './request_basic';
+import { mockBaseTimelineRequest } from './mocks/base_timeline_request';
+
+describe('timelineRequestBasicOptionsSchema', () => {
+ it('should correctly parse the base timeline request object', () => {
+ expect(timelineRequestBasicOptionsSchema.parse(mockBaseTimelineRequest)).toEqual(
+ mockBaseTimelineRequest
+ );
+ });
+
+ it('should correctly parse the base timeline request object and remove unknown fields', () => {
+ const invalidBaseTimelineRequest = {
+ ...mockBaseTimelineRequest,
+ iAmNotAllowed: 'butWhy?',
+ };
+ expect(timelineRequestBasicOptionsSchema.parse(invalidBaseTimelineRequest)).toEqual(
+ mockBaseTimelineRequest
+ );
+ });
+
+ it('should correctly error if an incorrect field type is provided for a schema key', () => {
+ const invalidBaseTimelineRequest = {
+ ...mockBaseTimelineRequest,
+ entityType: 'notAValidEntityType',
+ };
+
+ expect(() => {
+ timelineRequestBasicOptionsSchema.parse(invalidBaseTimelineRequest);
+ }).toThrowErrorMatchingInlineSnapshot(`
+ "[
+ {
+ \\"received\\": \\"notAValidEntityType\\",
+ \\"code\\": \\"invalid_enum_value\\",
+ \\"options\\": [
+ \\"events\\",
+ \\"sessions\\"
+ ],
+ \\"path\\": [
+ \\"entityType\\"
+ ],
+ \\"message\\": \\"Invalid enum value. Expected 'events' | 'sessions', received 'notAValidEntityType'\\"
+ }
+ ]"
+ `);
+ });
+});
diff --git a/x-pack/plugins/timelines/common/api/search_strategy/timeline/request_basic.ts b/x-pack/plugins/timelines/common/api/search_strategy/timeline/request_basic.ts
index 5e8ea1caaa0fb..c9c3145833572 100644
--- a/x-pack/plugins/timelines/common/api/search_strategy/timeline/request_basic.ts
+++ b/x-pack/plugins/timelines/common/api/search_strategy/timeline/request_basic.ts
@@ -12,6 +12,7 @@ import { timerange } from '../model/timerange';
export const timelineRequestBasicOptionsSchema = z.object({
indexType: z.string().optional(),
+ id: z.string().optional(),
timerange: timerange.optional(),
filterQuery,
defaultIndex: z.array(z.string()).optional(),
diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/import_connectors.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/import_connectors.ts
new file mode 100644
index 0000000000000..0dcda3af45510
--- /dev/null
+++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/import_connectors.ts
@@ -0,0 +1,517 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import expect from 'expect';
+
+import { DETECTION_ENGINE_RULES_URL } from '@kbn/security-solution-plugin/common/constants';
+import { FtrProviderContext } from '../../common/ftr_provider_context';
+import { createConnector, deleteConnector, getConnector } from '../../utils/connectors';
+import { combineToNdJson, deleteAllRules, getCustomQueryRuleParams } from '../../utils';
+
+// eslint-disable-next-line import/no-default-export
+export default ({ getService }: FtrProviderContext): void => {
+ const supertest = getService('supertest');
+ const log = getService('log');
+
+ describe('@ess @brokenInServerless @skipInQA import action connectors', () => {
+ const CONNECTOR_ID = '1be16246-642a-4ed8-bfd3-b47f8c7d7055';
+ const ANOTHER_CONNECTOR_ID = 'abc16246-642a-4ed8-bfd3-b47f8c7d7055';
+ const CUSTOM_ACTION_CONNECTOR = {
+ id: CONNECTOR_ID,
+ type: 'action',
+ updated_at: '2024-02-05T11:52:10.692Z',
+ created_at: '2024-02-05T11:52:10.692Z',
+ version: 'WzYsMV0=',
+ attributes: {
+ actionTypeId: '.email',
+ name: 'test-connector',
+ isMissingSecrets: false,
+ config: {
+ from: 'a@test.com',
+ service: 'other',
+ host: 'example.com',
+ port: 123,
+ secure: false,
+ hasAuth: false,
+ tenantId: null,
+ clientId: null,
+ oauthTokenUrl: null,
+ },
+ secrets: {},
+ },
+ references: [],
+ managed: false,
+ coreMigrationVersion: '8.8.0',
+ typeMigrationVersion: '8.3.0',
+ };
+
+ beforeEach(async () => {
+ await deleteAllRules(supertest, log);
+ await deleteConnector(supertest, CONNECTOR_ID);
+ await deleteConnector(supertest, ANOTHER_CONNECTOR_ID);
+ });
+
+ describe('overwrite connectors is set to "false"', () => {
+ it('imports a rule with an action connector', async () => {
+ const ndjson = combineToNdJson(
+ getCustomQueryRuleParams({
+ rule_id: 'rule-1',
+ name: 'Rule 1',
+ actions: [
+ {
+ group: 'default',
+ id: CONNECTOR_ID,
+ params: {
+ message: 'Some message',
+ to: ['test@test.com'],
+ subject: 'Test',
+ },
+ action_type_id: '.email',
+ uuid: 'fda6721b-d3a4-4d2c-ad0c-18893759e096',
+ frequency: { summary: true, notifyWhen: 'onActiveAlert', throttle: null },
+ },
+ ],
+ }),
+ CUSTOM_ACTION_CONNECTOR
+ );
+
+ const { body } = await supertest
+ .post(`${DETECTION_ENGINE_RULES_URL}/_import`)
+ .set('kbn-xsrf', 'true')
+ .set('elastic-api-version', '2023-10-31')
+ .attach('file', Buffer.from(ndjson), 'rules.ndjson')
+ .expect(200);
+
+ expect(body).toMatchObject({
+ errors: [],
+ success: true,
+ success_count: 1,
+ rules_count: 1,
+ action_connectors_success: true,
+ action_connectors_success_count: 1,
+ action_connectors_errors: [],
+ action_connectors_warnings: [],
+ });
+
+ expect(await getConnector(supertest, CONNECTOR_ID)).toMatchObject({
+ id: CONNECTOR_ID,
+ name: 'test-connector',
+ });
+ });
+
+ it('DOES NOT import an action connector without rules', async () => {
+ const ndjson = combineToNdJson(CUSTOM_ACTION_CONNECTOR);
+
+ const { body } = await supertest
+ .post(`${DETECTION_ENGINE_RULES_URL}/_import`)
+ .set('kbn-xsrf', 'true')
+ .set('elastic-api-version', '2023-10-31')
+ .attach('file', Buffer.from(ndjson), 'rules.ndjson')
+ .expect(200);
+
+ expect(body).toMatchObject({
+ errors: [],
+ success: true,
+ success_count: 0,
+ rules_count: 0,
+ action_connectors_success: true,
+ action_connectors_success_count: 0,
+ action_connectors_errors: [],
+ action_connectors_warnings: [],
+ });
+
+ await supertest
+ .get(`/api/actions/connector/${CONNECTOR_ID}`)
+ .set('kbn-xsrf', 'foo')
+ .expect(404);
+ });
+
+ it('DOES NOT import an action connector when there are no rules referencing it', async () => {
+ const ndjson = combineToNdJson(
+ getCustomQueryRuleParams({
+ rule_id: 'rule-1',
+ name: 'Rule 1',
+ actions: [
+ {
+ group: 'default',
+ id: ANOTHER_CONNECTOR_ID,
+ params: {
+ message: 'Some message',
+ to: ['test@test.com'],
+ subject: 'Test',
+ },
+ action_type_id: '.email',
+ uuid: 'fda6721b-d3a4-4d2c-ad0c-18893759e096',
+ frequency: { summary: true, notifyWhen: 'onActiveAlert', throttle: null },
+ },
+ ],
+ }),
+ { ...CUSTOM_ACTION_CONNECTOR, id: ANOTHER_CONNECTOR_ID },
+ CUSTOM_ACTION_CONNECTOR
+ );
+
+ const { body } = await supertest
+ .post(`${DETECTION_ENGINE_RULES_URL}/_import`)
+ .set('kbn-xsrf', 'true')
+ .set('elastic-api-version', '2023-10-31')
+ .attach('file', Buffer.from(ndjson), 'rules.ndjson')
+ .expect(200);
+
+ expect(body).toMatchObject({
+ errors: [],
+ success: true,
+ success_count: 1,
+ rules_count: 1,
+ action_connectors_success: true,
+ action_connectors_success_count: 1,
+ action_connectors_errors: [],
+ action_connectors_warnings: [],
+ });
+
+ await supertest
+ .get(`/api/actions/connector/${CONNECTOR_ID}`)
+ .set('kbn-xsrf', 'foo')
+ .expect(404);
+ });
+
+ it('DOES NOT return an error when rule actions reference a preconfigured connector', async () => {
+ const ndjson = combineToNdJson(
+ getCustomQueryRuleParams({
+ rule_id: 'rule-1',
+ name: 'Rule 1',
+ actions: [
+ {
+ group: 'default',
+ id: 'my-test-email',
+ params: {
+ message: 'Some message',
+ to: ['test@test.com'],
+ subject: 'Test',
+ },
+ action_type_id: '.email',
+ uuid: 'fda6721b-d3a4-4d2c-ad0c-18893759e096',
+ frequency: { summary: true, notifyWhen: 'onActiveAlert', throttle: null },
+ },
+ ],
+ })
+ );
+
+ const { body } = await supertest
+ .post(`${DETECTION_ENGINE_RULES_URL}/_import`)
+ .set('kbn-xsrf', 'true')
+ .set('elastic-api-version', '2023-10-31')
+ .attach('file', Buffer.from(ndjson), 'rules.ndjson')
+ .expect(200);
+
+ expect(body).toMatchObject({
+ errors: [],
+ success: true,
+ success_count: 1,
+ rules_count: 1,
+ action_connectors_success: true,
+ action_connectors_success_count: 0,
+ action_connectors_errors: [],
+ action_connectors_warnings: [],
+ });
+ });
+
+ /**
+ * When importing an action connector, if its `id` matches with an existing one, the type and config isn't checked.
+ * In fact, the connector being imported can have a different type and configuration, and its creation will be skipped.
+ */
+ it('skips importing already existing action connectors', async () => {
+ await createConnector(
+ supertest,
+ {
+ connector_type_id: '.webhook',
+ name: 'test-connector',
+ config: {
+ // checkout `x-pack/test/security_solution_api_integration/config/ess/config.base.ts` for configuration
+ // `some.non.existent.com` must be set as an allowed host
+ url: 'https://some.non.existent.com',
+ method: 'post',
+ },
+ secrets: {},
+ },
+ CONNECTOR_ID
+ );
+
+ const ndjson = combineToNdJson(
+ getCustomQueryRuleParams({
+ rule_id: 'rule-1',
+ name: 'Rule 1',
+ actions: [
+ {
+ group: 'default',
+ id: CONNECTOR_ID,
+ params: {
+ message: 'Some message',
+ to: ['test@test.com'],
+ subject: 'Test',
+ },
+ action_type_id: '.email',
+ uuid: 'fda6721b-d3a4-4d2c-ad0c-18893759e096',
+ frequency: { summary: true, notifyWhen: 'onActiveAlert', throttle: null },
+ },
+ ],
+ }),
+ CUSTOM_ACTION_CONNECTOR
+ );
+
+ const { body } = await supertest
+ .post(`${DETECTION_ENGINE_RULES_URL}/_import`)
+ .set('kbn-xsrf', 'true')
+ .set('elastic-api-version', '2023-10-31')
+ .attach('file', Buffer.from(ndjson), 'rules.ndjson')
+ .expect(200);
+
+ expect(body).toMatchObject({
+ errors: [],
+ success: true,
+ success_count: 1,
+ rules_count: 1,
+ action_connectors_success: true,
+ action_connectors_success_count: 0,
+ action_connectors_errors: [],
+ action_connectors_warnings: [],
+ });
+
+ expect(await getConnector(supertest, CONNECTOR_ID)).toMatchObject({
+ id: CONNECTOR_ID,
+ name: 'test-connector',
+ });
+ });
+
+ it('returns an error when connector is missing in ndjson', async () => {
+ const ndjson = combineToNdJson(
+ getCustomQueryRuleParams({
+ rule_id: 'rule-1',
+ name: 'Rule 1',
+ actions: [
+ {
+ group: 'default',
+ id: CONNECTOR_ID,
+ params: {
+ message: 'Some message',
+ to: ['test@test.com'],
+ subject: 'Test',
+ },
+ action_type_id: '.email',
+ uuid: 'fda6721b-d3a4-4d2c-ad0c-18893759e096',
+ frequency: { summary: true, notifyWhen: 'onActiveAlert', throttle: null },
+ },
+ ],
+ })
+ );
+
+ const { body } = await supertest
+ .post(`${DETECTION_ENGINE_RULES_URL}/_import`)
+ .set('kbn-xsrf', 'true')
+ .set('elastic-api-version', '2023-10-31')
+ .attach('file', Buffer.from(ndjson), 'rules.ndjson')
+ .expect(200);
+
+ expect(body).toMatchObject({
+ errors: [
+ {
+ error: {
+ message: `1 connector is missing. Connector id missing is: ${CONNECTOR_ID}`,
+ status_code: 404,
+ },
+ id: CONNECTOR_ID,
+ rule_id: 'rule-1',
+ },
+ ],
+ success: false,
+ success_count: 0,
+ rules_count: 1,
+ action_connectors_success: false,
+ action_connectors_success_count: 0,
+ action_connectors_errors: [
+ {
+ error: {
+ message: `1 connector is missing. Connector id missing is: ${CONNECTOR_ID}`,
+ status_code: 404,
+ },
+ id: CONNECTOR_ID,
+ rule_id: 'rule-1',
+ },
+ ],
+ action_connectors_warnings: [],
+ });
+ });
+ });
+
+ describe('overwrite connectors is set to "true"', () => {
+ it('overwrites existing connector', async () => {
+ await createConnector(
+ supertest,
+ {
+ connector_type_id: '.webhook',
+ name: 'existing-connector',
+ config: {
+ // checkout `x-pack/test/security_solution_api_integration/config/ess/config.base.ts` for configuration
+ // `some.non.existent.com` must be set as an allowed host
+ url: 'https://some.non.existent.com',
+ method: 'post',
+ },
+ secrets: {},
+ },
+ CONNECTOR_ID
+ );
+
+ const ndjson = combineToNdJson(
+ getCustomQueryRuleParams({
+ rule_id: 'rule-1',
+ name: 'Rule 1',
+ actions: [
+ {
+ group: 'default',
+ id: CONNECTOR_ID,
+ params: {
+ message: 'Some message',
+ to: ['test@test.com'],
+ subject: 'Test',
+ },
+ action_type_id: '.email',
+ uuid: 'fda6721b-d3a4-4d2c-ad0c-18893759e096',
+ frequency: { summary: true, notifyWhen: 'onActiveAlert', throttle: null },
+ },
+ ],
+ }),
+ {
+ ...CUSTOM_ACTION_CONNECTOR,
+ attributes: { ...CUSTOM_ACTION_CONNECTOR.attributes, name: 'updated-connector' },
+ }
+ );
+
+ const { body } = await supertest
+ .post(`${DETECTION_ENGINE_RULES_URL}/_import?overwrite_action_connectors=true`)
+ .set('kbn-xsrf', 'true')
+ .set('elastic-api-version', '2023-10-31')
+ .attach('file', Buffer.from(ndjson), 'rules.ndjson')
+ .expect(200);
+
+ expect(body).toMatchObject({
+ errors: [],
+ success: true,
+ success_count: 1,
+ rules_count: 1,
+ action_connectors_success: true,
+ action_connectors_success_count: 1,
+ action_connectors_errors: [],
+ action_connectors_warnings: [],
+ });
+
+ expect(await getConnector(supertest, CONNECTOR_ID)).toMatchObject({
+ id: CONNECTOR_ID,
+ name: 'updated-connector',
+ });
+ });
+
+ it('returns an error when connector is missing in ndjson', async () => {
+ const ndjson = combineToNdJson(
+ getCustomQueryRuleParams({
+ rule_id: 'rule-1',
+ name: 'Rule 1',
+ actions: [
+ {
+ group: 'default',
+ id: CONNECTOR_ID,
+ params: {
+ message: 'Some message',
+ to: ['test@test.com'],
+ subject: 'Test',
+ },
+ action_type_id: '.email',
+ uuid: 'fda6721b-d3a4-4d2c-ad0c-18893759e096',
+ frequency: { summary: true, notifyWhen: 'onActiveAlert', throttle: null },
+ },
+ ],
+ })
+ );
+
+ const { body } = await supertest
+ .post(`${DETECTION_ENGINE_RULES_URL}/_import?overwrite_action_connectors=true`)
+ .set('kbn-xsrf', 'true')
+ .set('elastic-api-version', '2023-10-31')
+ .attach('file', Buffer.from(ndjson), 'rules.ndjson')
+ .expect(200);
+
+ expect(body).toMatchObject({
+ errors: [
+ {
+ error: {
+ message: `1 connector is missing. Connector id missing is: ${CONNECTOR_ID}`,
+ status_code: 404,
+ },
+ id: CONNECTOR_ID,
+ rule_id: 'rule-1',
+ },
+ ],
+ success: false,
+ success_count: 0,
+ rules_count: 1,
+ action_connectors_success: false,
+ action_connectors_success_count: 0,
+ action_connectors_errors: [
+ {
+ error: {
+ message: `1 connector is missing. Connector id missing is: ${CONNECTOR_ID}`,
+ status_code: 404,
+ },
+ id: CONNECTOR_ID,
+ rule_id: 'rule-1',
+ },
+ ],
+ action_connectors_warnings: [],
+ });
+ });
+
+ it('DOES NOT return an error when rule actions reference a preconfigured connector', async () => {
+ const ndjson = combineToNdJson(
+ getCustomQueryRuleParams({
+ rule_id: 'rule-1',
+ name: 'Rule 1',
+ actions: [
+ {
+ group: 'default',
+ id: 'my-test-email',
+ params: {
+ message: 'Some message',
+ to: ['test@test.com'],
+ subject: 'Test',
+ },
+ action_type_id: '.email',
+ uuid: 'fda6721b-d3a4-4d2c-ad0c-18893759e096',
+ frequency: { summary: true, notifyWhen: 'onActiveAlert', throttle: null },
+ },
+ ],
+ })
+ );
+
+ const { body } = await supertest
+ .post(`${DETECTION_ENGINE_RULES_URL}/_import?overwrite_action_connectors=true`)
+ .set('kbn-xsrf', 'true')
+ .set('elastic-api-version', '2023-10-31')
+ .attach('file', Buffer.from(ndjson), 'rules.ndjson')
+ .expect(200);
+
+ expect(body).toMatchObject({
+ errors: [],
+ success: true,
+ success_count: 1,
+ rules_count: 1,
+ action_connectors_success: true,
+ action_connectors_success_count: 0,
+ action_connectors_errors: [],
+ action_connectors_warnings: [],
+ });
+ });
+ });
+ });
+};
diff --git a/x-pack/test/detection_engine_api_integration/utils/combine_to_ndjson.ts b/x-pack/test/detection_engine_api_integration/utils/combine_to_ndjson.ts
new file mode 100644
index 0000000000000..fc2baff9c365f
--- /dev/null
+++ b/x-pack/test/detection_engine_api_integration/utils/combine_to_ndjson.ts
@@ -0,0 +1,10 @@
+/*
+ * 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.
+ */
+
+export function combineToNdJson(...parts: unknown[]): string {
+ return parts.map((p) => JSON.stringify(p)).join('\n');
+}
diff --git a/x-pack/test/detection_engine_api_integration/utils/connectors/create_connector.ts b/x-pack/test/detection_engine_api_integration/utils/connectors/create_connector.ts
new file mode 100644
index 0000000000000..9c3f54e019653
--- /dev/null
+++ b/x-pack/test/detection_engine_api_integration/utils/connectors/create_connector.ts
@@ -0,0 +1,27 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import type SuperTest from 'supertest';
+
+interface CreateConnectorBody {
+ readonly name: string;
+ readonly config: Record;
+ readonly connector_type_id: string;
+ readonly secrets: Record;
+}
+
+export async function createConnector(
+ supertest: SuperTest.SuperTest,
+ connector: CreateConnectorBody,
+ id = ''
+): Promise {
+ await supertest
+ .post(`/api/actions/connector/${id}`)
+ .set('kbn-xsrf', 'foo')
+ .send(connector)
+ .expect(200);
+}
diff --git a/x-pack/test/detection_engine_api_integration/utils/connectors/delete_connector.ts b/x-pack/test/detection_engine_api_integration/utils/connectors/delete_connector.ts
new file mode 100644
index 0000000000000..683f845fd8bf8
--- /dev/null
+++ b/x-pack/test/detection_engine_api_integration/utils/connectors/delete_connector.ts
@@ -0,0 +1,15 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import type SuperTest from 'supertest';
+
+export function deleteConnector(
+ supertest: SuperTest.SuperTest,
+ connectorId: string
+): SuperTest.Test {
+ return supertest.delete(`/api/actions/connector/${connectorId}`).set('kbn-xsrf', 'foo');
+}
diff --git a/x-pack/test/detection_engine_api_integration/utils/connectors/get_connector.ts b/x-pack/test/detection_engine_api_integration/utils/connectors/get_connector.ts
new file mode 100644
index 0000000000000..8f7e4830372f9
--- /dev/null
+++ b/x-pack/test/detection_engine_api_integration/utils/connectors/get_connector.ts
@@ -0,0 +1,21 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { Connector } from '@kbn/actions-plugin/server/application/connector/types';
+import type SuperTest from 'supertest';
+
+export async function getConnector(
+ supertest: SuperTest.SuperTest,
+ connectorId: string
+): Promise {
+ const response = await supertest
+ .get(`/api/actions/connector/${connectorId}`)
+ .set('kbn-xsrf', 'foo')
+ .expect(200);
+
+ return response.body;
+}
diff --git a/x-pack/test/detection_engine_api_integration/utils/connectors/index.ts b/x-pack/test/detection_engine_api_integration/utils/connectors/index.ts
new file mode 100644
index 0000000000000..be89cd4a94d47
--- /dev/null
+++ b/x-pack/test/detection_engine_api_integration/utils/connectors/index.ts
@@ -0,0 +1,10 @@
+/*
+ * 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.
+ */
+
+export * from './create_connector';
+export * from './get_connector';
+export * from './delete_connector';
diff --git a/x-pack/test/detection_engine_api_integration/utils/get_custom_query_rule_params.ts b/x-pack/test/detection_engine_api_integration/utils/get_custom_query_rule_params.ts
new file mode 100644
index 0000000000000..d4773a4f7e516
--- /dev/null
+++ b/x-pack/test/detection_engine_api_integration/utils/get_custom_query_rule_params.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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import type { QueryRuleCreateProps } from '@kbn/security-solution-plugin/common/api/detection_engine';
+
+type CreateRulePropsRewrites = Partial>;
+
+/**
+ * Returns custom query rule params that is easy for most basic testing of output of alerts.
+ * It starts out in an disabled state. The 'from' is set very far back to test the basics of signal
+ * creation and testing by getting all the signals at once.
+ *
+ * @param rewrites rule params rewrites, see QueryRuleCreateProps for possible fields
+ */
+export function getCustomQueryRuleParams(
+ rewrites?: CreateRulePropsRewrites
+): QueryRuleCreateProps {
+ return {
+ type: 'query',
+ query: '*:*',
+ name: 'Custom query rule',
+ description: 'Custom query rule description',
+ risk_score: 1,
+ rule_id: 'rule-1',
+ severity: 'high',
+ index: ['logs-*'],
+ interval: '100m',
+ from: 'now-6m',
+ enabled: false,
+ ...rewrites,
+ };
+}
diff --git a/x-pack/test/detection_engine_api_integration/utils/index.ts b/x-pack/test/detection_engine_api_integration/utils/index.ts
index 47823f76ea6b3..7a17f692540ce 100644
--- a/x-pack/test/detection_engine_api_integration/utils/index.ts
+++ b/x-pack/test/detection_engine_api_integration/utils/index.ts
@@ -89,3 +89,5 @@ export * from './prebuilt_rules/install_mock_prebuilt_rules';
export * from './prebuilt_rules/install_prebuilt_rules_and_timelines';
export * from './get_legacy_action_so';
export * from './delete_all_exceptions';
+export * from './combine_to_ndjson';
+export * from './get_custom_query_rule_params';
diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/alerts/migrations/index.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/alerts/migrations/index.ts
index 115c8dbeab1f2..f57649c8ebbb0 100644
--- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/alerts/migrations/index.ts
+++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/default_license/alerts/migrations/index.ts
@@ -11,6 +11,6 @@ export default function ({ loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./create_alerts_migrations'));
loadTestFile(require.resolve('./delete_alerts_migrations'));
loadTestFile(require.resolve('./finalize_alerts_migrations'));
- loadTestFile(require.resolve('./finalize_alerts_migrations'));
+ loadTestFile(require.resolve('./get_alerts_migration_status'));
});
}
diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/combine_to_ndjson.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/combine_to_ndjson.ts
new file mode 100644
index 0000000000000..fc2baff9c365f
--- /dev/null
+++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/combine_to_ndjson.ts
@@ -0,0 +1,10 @@
+/*
+ * 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.
+ */
+
+export function combineToNdJson(...parts: unknown[]): string {
+ return parts.map((p) => JSON.stringify(p)).join('\n');
+}
diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/connectors/create_connector.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/connectors/create_connector.ts
new file mode 100644
index 0000000000000..9c3f54e019653
--- /dev/null
+++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/connectors/create_connector.ts
@@ -0,0 +1,27 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import type SuperTest from 'supertest';
+
+interface CreateConnectorBody {
+ readonly name: string;
+ readonly config: Record;
+ readonly connector_type_id: string;
+ readonly secrets: Record;
+}
+
+export async function createConnector(
+ supertest: SuperTest.SuperTest,
+ connector: CreateConnectorBody,
+ id = ''
+): Promise {
+ await supertest
+ .post(`/api/actions/connector/${id}`)
+ .set('kbn-xsrf', 'foo')
+ .send(connector)
+ .expect(200);
+}
diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/connectors/delete_connector.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/connectors/delete_connector.ts
new file mode 100644
index 0000000000000..683f845fd8bf8
--- /dev/null
+++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/connectors/delete_connector.ts
@@ -0,0 +1,15 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import type SuperTest from 'supertest';
+
+export function deleteConnector(
+ supertest: SuperTest.SuperTest,
+ connectorId: string
+): SuperTest.Test {
+ return supertest.delete(`/api/actions/connector/${connectorId}`).set('kbn-xsrf', 'foo');
+}
diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/connectors/get_connector.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/connectors/get_connector.ts
new file mode 100644
index 0000000000000..8f7e4830372f9
--- /dev/null
+++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/connectors/get_connector.ts
@@ -0,0 +1,21 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { Connector } from '@kbn/actions-plugin/server/application/connector/types';
+import type SuperTest from 'supertest';
+
+export async function getConnector(
+ supertest: SuperTest.SuperTest,
+ connectorId: string
+): Promise {
+ const response = await supertest
+ .get(`/api/actions/connector/${connectorId}`)
+ .set('kbn-xsrf', 'foo')
+ .expect(200);
+
+ return response.body;
+}
diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/connectors/index.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/connectors/index.ts
new file mode 100644
index 0000000000000..be89cd4a94d47
--- /dev/null
+++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/connectors/index.ts
@@ -0,0 +1,10 @@
+/*
+ * 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.
+ */
+
+export * from './create_connector';
+export * from './get_connector';
+export * from './delete_connector';
diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/index.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/index.ts
index afcdf130392f1..c16c3dc1f3674 100644
--- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/index.ts
+++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/index.ts
@@ -25,3 +25,4 @@ export * from './get_stats';
export * from './get_detection_metrics_from_body';
export * from './get_stats_url';
export * from './retry';
+export * from './combine_to_ndjson';