mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
## Summary [Internal link](https://github.com/elastic/security-team/issues/10820) to the feature details These changes add sorting functionality to the migration rules table. It is possible to sort migration rules by next columns: `Updated`, `Name`, `Status`, `Risk Score`, `Severity` and `Author`. ### Other changes Next fixes and adjustments were also implemented as part of this PR: * `Installed` status in migration rules table to indicate whether the rule was installed * Rules selection and installation of selected rules * Disable selection for not fully translated rules * `Author` column to show whether the translated rule matched one of the existing Elastic prebuilt rules * `Install and enable` and `Install without enabling` buttons within the migration rule details flyout
This commit is contained in:
parent
ebb4f503a5
commit
70a5bb33c4
30 changed files with 541 additions and 122 deletions
|
@ -46,6 +46,7 @@ export type FieldMap<T extends string = string> = Record<
|
|||
array?: boolean;
|
||||
doc_values?: boolean;
|
||||
enabled?: boolean;
|
||||
fields?: Record<string, { type: string }>;
|
||||
format?: string;
|
||||
ignore_above?: number;
|
||||
multi_fields?: MultiField[];
|
||||
|
|
|
@ -59,6 +59,8 @@ export type GetRuleMigrationRequestQuery = z.infer<typeof GetRuleMigrationReques
|
|||
export const GetRuleMigrationRequestQuery = z.object({
|
||||
page: z.coerce.number().optional(),
|
||||
per_page: z.coerce.number().optional(),
|
||||
sort_field: NonEmptyString.optional(),
|
||||
sort_direction: z.enum(['asc', 'desc']).optional(),
|
||||
search_term: z.string().optional(),
|
||||
});
|
||||
export type GetRuleMigrationRequestQueryInput = z.input<typeof GetRuleMigrationRequestQuery>;
|
||||
|
@ -154,7 +156,13 @@ export type InstallMigrationRulesRequestParamsInput = z.input<
|
|||
>;
|
||||
|
||||
export type InstallMigrationRulesRequestBody = z.infer<typeof InstallMigrationRulesRequestBody>;
|
||||
export const InstallMigrationRulesRequestBody = z.array(NonEmptyString);
|
||||
export const InstallMigrationRulesRequestBody = z.object({
|
||||
ids: z.array(NonEmptyString),
|
||||
/**
|
||||
* Indicates whether installed rules should be enabled
|
||||
*/
|
||||
enabled: z.boolean().optional(),
|
||||
});
|
||||
export type InstallMigrationRulesRequestBodyInput = z.input<
|
||||
typeof InstallMigrationRulesRequestBody
|
||||
>;
|
||||
|
|
|
@ -133,6 +133,19 @@ paths:
|
|||
required: false
|
||||
schema:
|
||||
type: number
|
||||
- name: sort_field
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
$ref: '../../../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString'
|
||||
- name: sort_direction
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
enum:
|
||||
- asc
|
||||
- desc
|
||||
- name: search_term
|
||||
in: query
|
||||
required: false
|
||||
|
@ -180,10 +193,18 @@ paths:
|
|||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
description: The rule migration id
|
||||
$ref: '../../../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString'
|
||||
type: object
|
||||
required:
|
||||
- ids
|
||||
properties:
|
||||
ids:
|
||||
type: array
|
||||
items:
|
||||
description: The rule migration id
|
||||
$ref: '../../../../../common/api/model/primitives.schema.yaml#/components/schemas/NonEmptyString'
|
||||
enabled:
|
||||
type: boolean
|
||||
description: Indicates whether installed rules should be enabled
|
||||
responses:
|
||||
200:
|
||||
description: Indicates rules migrations have been installed correctly.
|
||||
|
|
|
@ -22,13 +22,17 @@ export const isMigrationCustomRule = (rule?: ElasticRule): rule is MigrationCust
|
|||
!isMigrationPrebuiltRule(rule) &&
|
||||
!!(rule?.title && rule?.description && rule?.query && rule?.query_language);
|
||||
|
||||
export const convertMigrationCustomRuleToSecurityRulePayload = (rule: MigrationCustomRule) => {
|
||||
export const convertMigrationCustomRuleToSecurityRulePayload = (
|
||||
rule: MigrationCustomRule,
|
||||
enabled: boolean
|
||||
) => {
|
||||
return {
|
||||
type: rule.query_language,
|
||||
language: rule.query_language,
|
||||
query: rule.query,
|
||||
name: rule.title,
|
||||
description: rule.description,
|
||||
enabled,
|
||||
|
||||
...DEFAULT_TRANSLATION_FIELDS,
|
||||
severity: (rule.severity as Severity) ?? DEFAULT_TRANSLATION_SEVERITY,
|
||||
|
|
|
@ -120,6 +120,10 @@ export interface GetRuleMigrationParams {
|
|||
page?: number;
|
||||
/** Optional number of documents per page to retrieve */
|
||||
perPage?: number;
|
||||
/** Optional field of the rule migration object to sort results by */
|
||||
sortField?: string;
|
||||
/** Optional direction to sort results by */
|
||||
sortDirection?: 'asc' | 'desc';
|
||||
/** Optional search term to filter documents */
|
||||
searchTerm?: string;
|
||||
/** Optional AbortSignal for cancelling request */
|
||||
|
@ -130,12 +134,24 @@ export const getRuleMigrations = async ({
|
|||
migrationId,
|
||||
page,
|
||||
perPage,
|
||||
sortField,
|
||||
sortDirection,
|
||||
searchTerm,
|
||||
signal,
|
||||
}: GetRuleMigrationParams): Promise<GetRuleMigrationResponse> => {
|
||||
return KibanaServices.get().http.get<GetRuleMigrationResponse>(
|
||||
replaceParams(SIEM_RULE_MIGRATION_PATH, { migration_id: migrationId }),
|
||||
{ version: '1', query: { page, per_page: perPage, search_term: searchTerm }, signal }
|
||||
{
|
||||
version: '1',
|
||||
query: {
|
||||
page,
|
||||
per_page: perPage,
|
||||
sort_field: sortField,
|
||||
sort_direction: sortDirection,
|
||||
search_term: searchTerm,
|
||||
},
|
||||
signal,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -163,6 +179,8 @@ export interface InstallRulesParams {
|
|||
migrationId: string;
|
||||
/** The rule ids to install */
|
||||
ids: string[];
|
||||
/** Optional indicator to enable the installed rule */
|
||||
enabled?: boolean;
|
||||
/** Optional AbortSignal for cancelling request */
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
|
@ -170,11 +188,12 @@ export interface InstallRulesParams {
|
|||
export const installMigrationRules = async ({
|
||||
migrationId,
|
||||
ids,
|
||||
enabled,
|
||||
signal,
|
||||
}: InstallRulesParams): Promise<InstallMigrationRulesResponse> => {
|
||||
return KibanaServices.get().http.post<InstallMigrationRulesResponse>(
|
||||
replaceParams(SIEM_RULE_MIGRATION_INSTALL_PATH, { migration_id: migrationId }),
|
||||
{ version: '1', body: JSON.stringify(ids), signal }
|
||||
{ version: '1', body: JSON.stringify({ ids, enabled }), signal }
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -84,7 +84,8 @@ export const MigrationRuleDetailsFlyout: React.FC<MigrationRuleDetailsFlyoutProp
|
|||
const rule = useMemo(() => {
|
||||
if (isMigrationCustomRule(ruleMigration.elastic_rule)) {
|
||||
return convertMigrationCustomRuleToSecurityRulePayload(
|
||||
ruleMigration.elastic_rule
|
||||
ruleMigration.elastic_rule,
|
||||
false
|
||||
) as RuleResponse; // TODO: we need to adjust RuleOverviewTab to allow partial RuleResponse as a parameter;
|
||||
}
|
||||
return matchedPrebuiltRule;
|
||||
|
|
|
@ -6,7 +6,13 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiLoadingSpinner,
|
||||
} from '@elastic/eui';
|
||||
import * as i18n from './translations';
|
||||
|
||||
export interface BulkActionsProps {
|
||||
|
@ -29,13 +35,14 @@ export const BulkActions: React.FC<BulkActionsProps> = React.memo(
|
|||
installSelectedRule,
|
||||
}) => {
|
||||
const disableInstallTranslatedRulesButton = isTableLoading || !numberOfTranslatedRules;
|
||||
const showInstallSelectedRulesButton =
|
||||
disableInstallTranslatedRulesButton && numberOfSelectedRules > 0;
|
||||
const showInstallSelectedRulesButton = isTableLoading || numberOfSelectedRules > 0;
|
||||
return (
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s" responsive={false} wrap={true}>
|
||||
{showInstallSelectedRulesButton ? (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
<EuiButtonEmpty
|
||||
iconType="plusInCircle"
|
||||
color={'primary'}
|
||||
onClick={installSelectedRule}
|
||||
disabled={isTableLoading}
|
||||
data-test-subj="installSelectedRulesButton"
|
||||
|
@ -43,7 +50,7 @@ export const BulkActions: React.FC<BulkActionsProps> = React.memo(
|
|||
>
|
||||
{i18n.INSTALL_SELECTED_RULES(numberOfSelectedRules)}
|
||||
{isTableLoading && <EuiLoadingSpinner size="s" />}
|
||||
</EuiButton>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
) : null}
|
||||
<EuiFlexItem grow={false}>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { CriteriaWithPagination } from '@elastic/eui';
|
||||
import type { CriteriaWithPagination, EuiTableSelectionType } from '@elastic/eui';
|
||||
import {
|
||||
EuiSkeletonLoading,
|
||||
EuiSkeletonTitle,
|
||||
|
@ -14,6 +14,7 @@ import {
|
|||
EuiFlexItem,
|
||||
EuiSpacer,
|
||||
EuiBasicTable,
|
||||
EuiButton,
|
||||
} from '@elastic/eui';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
|
@ -30,8 +31,12 @@ import { useGetMigrationPrebuiltRules } from '../../logic/use_get_migration_preb
|
|||
import * as logicI18n from '../../logic/translations';
|
||||
import { BulkActions } from './bulk_actions';
|
||||
import { SearchField } from './search_field';
|
||||
import { SiemMigrationRuleTranslationResult } from '../../../../../common/siem_migrations/constants';
|
||||
import * as i18n from './translations';
|
||||
|
||||
const DEFAULT_PAGE_SIZE = 10;
|
||||
const DEFAULT_SORT_FIELD = 'translation_result';
|
||||
const DEFAULT_SORT_DIRECTION = 'desc';
|
||||
|
||||
export interface MigrationRulesTableProps {
|
||||
/**
|
||||
|
@ -49,6 +54,8 @@ export const MigrationRulesTable: React.FC<MigrationRulesTableProps> = React.mem
|
|||
|
||||
const [pageIndex, setPageIndex] = useState(0);
|
||||
const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE);
|
||||
const [sortField, setSortField] = useState<keyof RuleMigration>(DEFAULT_SORT_FIELD);
|
||||
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>(DEFAULT_SORT_DIRECTION);
|
||||
const [searchTerm, setSearchTerm] = useState<string | undefined>();
|
||||
|
||||
const { data: translationStats, isLoading: isStatsLoading } =
|
||||
|
@ -64,10 +71,33 @@ export const MigrationRulesTable: React.FC<MigrationRulesTableProps> = React.mem
|
|||
migrationId,
|
||||
page: pageIndex,
|
||||
perPage: pageSize,
|
||||
sortField,
|
||||
sortDirection,
|
||||
searchTerm,
|
||||
});
|
||||
|
||||
const [selectedRuleMigrations, setSelectedRuleMigrations] = useState<RuleMigration[]>([]);
|
||||
const tableSelection: EuiTableSelectionType<RuleMigration> = useMemo(
|
||||
() => ({
|
||||
selectable: (item: RuleMigration) => {
|
||||
return (
|
||||
!item.elastic_rule?.id &&
|
||||
item.translation_result === SiemMigrationRuleTranslationResult.FULL
|
||||
);
|
||||
},
|
||||
selectableMessage: (selectable: boolean, item: RuleMigration) => {
|
||||
if (selectable) {
|
||||
return '';
|
||||
}
|
||||
return item.elastic_rule?.id
|
||||
? i18n.ALREADY_TRANSLATED_RULE_TOOLTIP
|
||||
: i18n.NOT_FULLY_TRANSLATED_RULE_TOOLTIP;
|
||||
},
|
||||
onSelectionChange: setSelectedRuleMigrations,
|
||||
selected: selectedRuleMigrations,
|
||||
}),
|
||||
[selectedRuleMigrations]
|
||||
);
|
||||
|
||||
const pagination = useMemo(() => {
|
||||
return {
|
||||
|
@ -77,11 +107,25 @@ export const MigrationRulesTable: React.FC<MigrationRulesTableProps> = React.mem
|
|||
};
|
||||
}, [pageIndex, pageSize, total]);
|
||||
|
||||
const sorting = useMemo(() => {
|
||||
return {
|
||||
sort: {
|
||||
field: sortField,
|
||||
direction: sortDirection,
|
||||
},
|
||||
};
|
||||
}, [sortDirection, sortField]);
|
||||
|
||||
const onTableChange = useCallback(({ page, sort }: CriteriaWithPagination<RuleMigration>) => {
|
||||
if (page) {
|
||||
setPageIndex(page.index);
|
||||
setPageSize(page.size);
|
||||
}
|
||||
if (sort) {
|
||||
const { field, direction } = sort;
|
||||
setSortField(field);
|
||||
setSortDirection(direction);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleOnSearch = useCallback((value: string) => {
|
||||
|
@ -94,10 +138,10 @@ export const MigrationRulesTable: React.FC<MigrationRulesTableProps> = React.mem
|
|||
|
||||
const [isTableLoading, setTableLoading] = useState(false);
|
||||
const installSingleRule = useCallback(
|
||||
async (migrationRule: RuleMigration, enable?: boolean) => {
|
||||
async (migrationRule: RuleMigration, enabled = false) => {
|
||||
setTableLoading(true);
|
||||
try {
|
||||
await installMigrationRules([migrationRule.id]);
|
||||
await installMigrationRules({ ids: [migrationRule.id], enabled });
|
||||
} catch (error) {
|
||||
addError(error, { title: logicI18n.INSTALL_MIGRATION_RULES_FAILURE });
|
||||
} finally {
|
||||
|
@ -107,6 +151,24 @@ export const MigrationRulesTable: React.FC<MigrationRulesTableProps> = React.mem
|
|||
[addError, installMigrationRules]
|
||||
);
|
||||
|
||||
const installSelectedRule = useCallback(
|
||||
async (enabled = false) => {
|
||||
setTableLoading(true);
|
||||
try {
|
||||
await installMigrationRules({
|
||||
ids: selectedRuleMigrations.map((rule) => rule.id),
|
||||
enabled,
|
||||
});
|
||||
} catch (error) {
|
||||
addError(error, { title: logicI18n.INSTALL_MIGRATION_RULES_FAILURE });
|
||||
} finally {
|
||||
setTableLoading(false);
|
||||
setSelectedRuleMigrations([]);
|
||||
}
|
||||
},
|
||||
[addError, installMigrationRules, selectedRuleMigrations]
|
||||
);
|
||||
|
||||
const installTranslatedRules = useCallback(
|
||||
async (enable?: boolean) => {
|
||||
setTableLoading(true);
|
||||
|
@ -121,12 +183,45 @@ export const MigrationRulesTable: React.FC<MigrationRulesTableProps> = React.mem
|
|||
[addError, installTranslatedMigrationRules]
|
||||
);
|
||||
|
||||
const isLoading = isStatsLoading || isPrebuiltRulesLoading || isDataLoading || isTableLoading;
|
||||
|
||||
const ruleActionsFactory = useCallback(
|
||||
(ruleMigration: RuleMigration, closeRulePreview: () => void) => {
|
||||
// TODO: Add flyout action buttons
|
||||
return null;
|
||||
const canMigrationRuleBeInstalled =
|
||||
!isLoading &&
|
||||
!ruleMigration.elastic_rule?.id &&
|
||||
ruleMigration.translation_result === SiemMigrationRuleTranslationResult.FULL;
|
||||
return (
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiButton
|
||||
disabled={!canMigrationRuleBeInstalled}
|
||||
onClick={() => {
|
||||
installSingleRule(ruleMigration);
|
||||
closeRulePreview();
|
||||
}}
|
||||
data-test-subj="installMigrationRuleFromFlyoutButton"
|
||||
>
|
||||
{i18n.INSTALL_WITHOUT_ENABLING_BUTTON_LABEL}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiButton
|
||||
disabled={!canMigrationRuleBeInstalled}
|
||||
onClick={() => {
|
||||
installSingleRule(ruleMigration, true);
|
||||
closeRulePreview();
|
||||
}}
|
||||
fill
|
||||
data-test-subj="installAndEnableMigrationRuleFromFlyoutButton"
|
||||
>
|
||||
{i18n.INSTALL_AND_ENABLE_BUTTON_LABEL}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
},
|
||||
[]
|
||||
[installSingleRule, isLoading]
|
||||
);
|
||||
|
||||
const {
|
||||
|
@ -143,8 +238,6 @@ export const MigrationRulesTable: React.FC<MigrationRulesTableProps> = React.mem
|
|||
installMigrationRule: installSingleRule,
|
||||
});
|
||||
|
||||
const isLoading = isStatsLoading || isPrebuiltRulesLoading || isDataLoading || isTableLoading;
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiSkeletonLoading
|
||||
|
@ -168,8 +261,9 @@ export const MigrationRulesTable: React.FC<MigrationRulesTableProps> = React.mem
|
|||
<BulkActions
|
||||
isTableLoading={isLoading}
|
||||
numberOfTranslatedRules={translationStats?.rules.installable ?? 0}
|
||||
numberOfSelectedRules={0}
|
||||
numberOfSelectedRules={selectedRuleMigrations.length}
|
||||
installTranslatedRule={installTranslatedRules}
|
||||
installSelectedRule={installSelectedRule}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
@ -178,12 +272,9 @@ export const MigrationRulesTable: React.FC<MigrationRulesTableProps> = React.mem
|
|||
loading={isTableLoading}
|
||||
items={ruleMigrations}
|
||||
pagination={pagination}
|
||||
sorting={sorting}
|
||||
onChange={onTableChange}
|
||||
selection={{
|
||||
selectable: () => true,
|
||||
onSelectionChange: setSelectedRuleMigrations,
|
||||
initialSelected: selectedRuleMigrations,
|
||||
}}
|
||||
selection={tableSelection}
|
||||
itemId={'id'}
|
||||
data-test-subj={'rules-translation-table'}
|
||||
columns={rulesColumns}
|
||||
|
|
|
@ -80,3 +80,31 @@ export const INSTALL_TRANSLATED_ARIA_LABEL = i18n.translate(
|
|||
defaultMessage: 'Install all translated rules',
|
||||
}
|
||||
);
|
||||
|
||||
export const ALREADY_TRANSLATED_RULE_TOOLTIP = i18n.translate(
|
||||
'xpack.securitySolution.siemMigrations.rules.table.alreadyTranslatedTooltip',
|
||||
{
|
||||
defaultMessage: 'Already translated migration rule',
|
||||
}
|
||||
);
|
||||
|
||||
export const NOT_FULLY_TRANSLATED_RULE_TOOLTIP = i18n.translate(
|
||||
'xpack.securitySolution.siemMigrations.rules.table.notFullyTranslatedTooltip',
|
||||
{
|
||||
defaultMessage: 'Not fully translated migration rule',
|
||||
}
|
||||
);
|
||||
|
||||
export const INSTALL_WITHOUT_ENABLING_BUTTON_LABEL = i18n.translate(
|
||||
'xpack.securitySolution.siemMigrations.rules.table.installWithoutEnablingButtonLabel',
|
||||
{
|
||||
defaultMessage: 'Install without enabling',
|
||||
}
|
||||
);
|
||||
|
||||
export const INSTALL_AND_ENABLE_BUTTON_LABEL = i18n.translate(
|
||||
'xpack.securitySolution.siemMigrations.rules.table.installAndEnableButtonLabel',
|
||||
{
|
||||
defaultMessage: 'Install and enable',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui';
|
||||
import type { RuleMigration } from '../../../../../common/siem_migrations/model/rule_migration.gen';
|
||||
import * as i18n from './translations';
|
||||
import type { TableColumn } from './constants';
|
||||
|
||||
const Author = ({ isPrebuiltRule }: { isPrebuiltRule: boolean }) => {
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center">
|
||||
{isPrebuiltRule && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon type="logoElastic" size="m" />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiFlexItem grow={false}>
|
||||
{isPrebuiltRule ? i18n.ELASTIC_AUTHOR_TITLE : i18n.CUSTOM_AUTHOR_TITLE}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
export const createAuthorColumn = (): TableColumn => {
|
||||
return {
|
||||
field: 'elastic_rule.prebuilt_rule_id',
|
||||
name: i18n.COLUMN_AUTHOR,
|
||||
render: (_, rule: RuleMigration) => {
|
||||
return <Author isPrebuiltRule={!!rule.elastic_rule?.prebuilt_rule_id} />;
|
||||
},
|
||||
sortable: true,
|
||||
width: '10%',
|
||||
};
|
||||
};
|
|
@ -8,6 +8,7 @@
|
|||
export * from './constants';
|
||||
|
||||
export * from './actions';
|
||||
export * from './author';
|
||||
export * from './name';
|
||||
export * from './risk_score';
|
||||
export * from './severity';
|
||||
|
|
|
@ -12,12 +12,11 @@ import * as i18n from './translations';
|
|||
import type { TableColumn } from './constants';
|
||||
|
||||
interface NameProps {
|
||||
name: string;
|
||||
rule: RuleMigration;
|
||||
openMigrationRuleDetails: (rule: RuleMigration) => void;
|
||||
}
|
||||
|
||||
const Name = ({ name, rule, openMigrationRuleDetails }: NameProps) => {
|
||||
const Name = ({ rule, openMigrationRuleDetails }: NameProps) => {
|
||||
return (
|
||||
<EuiLink
|
||||
onClick={() => {
|
||||
|
@ -25,7 +24,7 @@ const Name = ({ name, rule, openMigrationRuleDetails }: NameProps) => {
|
|||
}}
|
||||
data-test-subj="ruleName"
|
||||
>
|
||||
{name}
|
||||
{rule.elastic_rule?.title}
|
||||
</EuiLink>
|
||||
);
|
||||
};
|
||||
|
@ -36,10 +35,10 @@ export const createNameColumn = ({
|
|||
openMigrationRuleDetails: (rule: RuleMigration) => void;
|
||||
}): TableColumn => {
|
||||
return {
|
||||
field: 'original_rule.title',
|
||||
field: 'elastic_rule.title',
|
||||
name: i18n.COLUMN_NAME,
|
||||
render: (value: RuleMigration['original_rule']['title'], rule: RuleMigration) => (
|
||||
<Name name={value} rule={rule} openMigrationRuleDetails={openMigrationRuleDetails} />
|
||||
render: (_, rule: RuleMigration) => (
|
||||
<Name rule={rule} openMigrationRuleDetails={openMigrationRuleDetails} />
|
||||
),
|
||||
sortable: true,
|
||||
truncateText: true,
|
||||
|
|
|
@ -22,6 +22,6 @@ export const createRiskScoreColumn = (): TableColumn => {
|
|||
),
|
||||
sortable: true,
|
||||
truncateText: true,
|
||||
width: '75px',
|
||||
width: '10%',
|
||||
};
|
||||
};
|
||||
|
|
|
@ -8,9 +8,7 @@
|
|||
import React from 'react';
|
||||
import type { Severity } from '@kbn/securitysolution-io-ts-alerting-types';
|
||||
import { DEFAULT_TRANSLATION_SEVERITY } from '../../../../../common/siem_migrations/constants';
|
||||
import { getNormalizedSeverity } from '../../../../detection_engine/rule_management_ui/components/rules_table/helpers';
|
||||
import { SeverityBadge } from '../../../../common/components/severity_badge';
|
||||
import type { RuleMigration } from '../../../../../common/siem_migrations/model/rule_migration.gen';
|
||||
import type { TableColumn } from './constants';
|
||||
import * as i18n from './translations';
|
||||
|
||||
|
@ -19,8 +17,7 @@ export const createSeverityColumn = (): TableColumn => {
|
|||
field: 'elastic_rule.severity',
|
||||
name: i18n.COLUMN_SEVERITY,
|
||||
render: (value?: Severity) => <SeverityBadge value={value ?? DEFAULT_TRANSLATION_SEVERITY} />,
|
||||
sortable: ({ elastic_rule: elasticRule }: RuleMigration) =>
|
||||
getNormalizedSeverity((elasticRule?.severity as Severity) ?? DEFAULT_TRANSLATION_SEVERITY),
|
||||
sortable: true,
|
||||
truncateText: true,
|
||||
width: '12%',
|
||||
};
|
||||
|
|
|
@ -18,8 +18,8 @@ export const createStatusColumn = (): TableColumn => {
|
|||
render: (value: RuleMigration['translation_result'], rule: RuleMigration) => (
|
||||
<StatusBadge value={value} installedRuleId={rule.elastic_rule?.id} />
|
||||
),
|
||||
sortable: false,
|
||||
sortable: true,
|
||||
truncateText: true,
|
||||
width: '12%',
|
||||
width: '15%',
|
||||
};
|
||||
};
|
||||
|
|
|
@ -14,6 +14,27 @@ export const COLUMN_ACTIONS = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const COLUMN_AUTHOR = i18n.translate(
|
||||
'xpack.securitySolution.siemMigrations.rules.tableColumn.authorLabel',
|
||||
{
|
||||
defaultMessage: 'Author',
|
||||
}
|
||||
);
|
||||
|
||||
export const ELASTIC_AUTHOR_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.siemMigrations.rules.tableColumn.elasticAuthorTitle',
|
||||
{
|
||||
defaultMessage: 'Elastic',
|
||||
}
|
||||
);
|
||||
|
||||
export const CUSTOM_AUTHOR_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.siemMigrations.rules.tableColumn.customAuthorTitle',
|
||||
{
|
||||
defaultMessage: 'Custom',
|
||||
}
|
||||
);
|
||||
|
||||
export const ACTIONS_VIEW_LABEL = i18n.translate(
|
||||
'xpack.securitySolution.siemMigrations.rules.tableColumn.actionsViewLabel',
|
||||
{
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { StatusBadge } from '.';
|
||||
|
||||
describe('StatusBadge', () => {
|
||||
it('renders correctly', () => {
|
||||
const wrapper = shallow(<StatusBadge value="full" />);
|
||||
|
||||
expect(wrapper.find('HealthTruncateText')).toHaveLength(1);
|
||||
});
|
||||
});
|
|
@ -8,9 +8,16 @@
|
|||
import React from 'react';
|
||||
import { euiLightVars } from '@kbn/ui-theme';
|
||||
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiHealth, EuiIcon, EuiToolTip } from '@elastic/eui';
|
||||
import { css } from '@emotion/css';
|
||||
import type { RuleMigrationTranslationResult } from '../../../../../common/siem_migrations/model/rule_migration.gen';
|
||||
import { HealthTruncateText } from '../../../../common/components/health_truncate_text';
|
||||
import { convertTranslationResultIntoText } from '../../utils/helpers';
|
||||
import * as i18n from './translations';
|
||||
|
||||
const statusTextWrapperClassName = css`
|
||||
width: 100%;
|
||||
display: inline-grid;
|
||||
`;
|
||||
|
||||
const { euiColorVis0, euiColorVis7, euiColorVis9 } = euiLightVars;
|
||||
const statusToColorMap: Record<RuleMigrationTranslationResult, string> = {
|
||||
|
@ -28,17 +35,28 @@ interface StatusBadgeProps {
|
|||
export const StatusBadge: React.FC<StatusBadgeProps> = React.memo(
|
||||
({ value, installedRuleId, 'data-test-subj': dataTestSubj = 'translation-result' }) => {
|
||||
const translationResult = installedRuleId ? 'full' : value ?? 'untranslatable';
|
||||
const displayValue = convertTranslationResultIntoText(translationResult);
|
||||
const displayValue = installedRuleId
|
||||
? i18n.RULE_STATUS_INSTALLED
|
||||
: convertTranslationResultIntoText(translationResult);
|
||||
const color = statusToColorMap[translationResult];
|
||||
|
||||
return (
|
||||
<HealthTruncateText
|
||||
healthColor={color}
|
||||
tooltipContent={displayValue}
|
||||
dataTestSubj={dataTestSubj}
|
||||
>
|
||||
{displayValue}
|
||||
</HealthTruncateText>
|
||||
<EuiToolTip content={displayValue}>
|
||||
{installedRuleId ? (
|
||||
<EuiFlexGroup gutterSize="xs" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon type={'check'} color={statusToColorMap.full} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>{displayValue}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
) : (
|
||||
<EuiHealth color={color} data-test-subj={dataTestSubj}>
|
||||
<div className={statusTextWrapperClassName}>
|
||||
<span className="eui-textTruncate">{displayValue}</span>
|
||||
</div>
|
||||
</EuiHealth>
|
||||
)}
|
||||
</EuiToolTip>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const RULE_STATUS_INSTALLED = i18n.translate(
|
||||
'xpack.securitySolution.siemMigrations.rules.status.installedLabel',
|
||||
{
|
||||
defaultMessage: 'Installed',
|
||||
}
|
||||
);
|
|
@ -10,6 +10,7 @@ import type { RuleMigration } from '../../../../common/siem_migrations/model/rul
|
|||
import type { TableColumn } from '../components/rules_table_columns';
|
||||
import {
|
||||
createActionsColumn,
|
||||
createAuthorColumn,
|
||||
createNameColumn,
|
||||
createRiskScoreColumn,
|
||||
createSeverityColumn,
|
||||
|
@ -33,6 +34,7 @@ export const useMigrationRulesTableColumns = ({
|
|||
createStatusColumn(),
|
||||
createRiskScoreColumn(),
|
||||
createSeverityColumn(),
|
||||
createAuthorColumn(),
|
||||
createActionsColumn({
|
||||
disableActions,
|
||||
openMigrationRuleDetails,
|
||||
|
|
|
@ -18,6 +18,8 @@ export const useGetMigrationRules = (params: {
|
|||
migrationId: string;
|
||||
page?: number;
|
||||
perPage?: number;
|
||||
sortField: string;
|
||||
sortDirection: 'asc' | 'desc';
|
||||
searchTerm?: string;
|
||||
}) => {
|
||||
const { addError } = useAppToasts();
|
||||
|
|
|
@ -23,8 +23,8 @@ export const useInstallMigrationRules = (migrationId: string) => {
|
|||
const invalidateGetMigrationTranslationStats =
|
||||
useInvalidateGetMigrationTranslationStats(migrationId);
|
||||
|
||||
return useMutation<InstallMigrationRulesResponse, Error, string[]>(
|
||||
(ids: string[]) => installMigrationRules({ migrationId, ids }),
|
||||
return useMutation<InstallMigrationRulesResponse, Error, { ids: string[]; enabled: boolean }>(
|
||||
({ ids, enabled = false }) => installMigrationRules({ migrationId, ids, enabled }),
|
||||
{
|
||||
mutationKey: INSTALL_MIGRATION_RULES_MUTATION_KEY,
|
||||
onError: (error) => {
|
||||
|
|
|
@ -39,13 +39,20 @@ export const registerSiemRuleMigrationsGetRoute = (
|
|||
},
|
||||
withLicense(async (context, req, res): Promise<IKibanaResponse<GetRuleMigrationResponse>> => {
|
||||
const { migration_id: migrationId } = req.params;
|
||||
const { page, per_page: perPage, search_term: searchTerm } = req.query;
|
||||
const {
|
||||
page,
|
||||
per_page: perPage,
|
||||
sort_field: sortField,
|
||||
sort_direction: sortDirection,
|
||||
search_term: searchTerm,
|
||||
} = req.query;
|
||||
try {
|
||||
const ctx = await context.resolve(['securitySolution']);
|
||||
const ruleMigrationsClient = ctx.securitySolution.getSiemRuleMigrationsClient();
|
||||
|
||||
const options: RuleMigrationGetOptions = {
|
||||
filters: { searchTerm },
|
||||
sort: { sortField, sortDirection },
|
||||
size: perPage,
|
||||
from: page && perPage ? page * perPage : 0,
|
||||
};
|
||||
|
|
|
@ -40,7 +40,7 @@ export const registerSiemRuleMigrationsInstallRoute = (
|
|||
withLicense(
|
||||
async (context, req, res): Promise<IKibanaResponse<InstallMigrationRulesResponse>> => {
|
||||
const { migration_id: migrationId } = req.params;
|
||||
const ids = req.body;
|
||||
const { ids, enabled = false } = req.body;
|
||||
|
||||
try {
|
||||
const ctx = await context.resolve(['core', 'alerting', 'securitySolution']);
|
||||
|
@ -52,6 +52,7 @@ export const registerSiemRuleMigrationsInstallRoute = (
|
|||
await installTranslated({
|
||||
migrationId,
|
||||
ids,
|
||||
enabled,
|
||||
securitySolutionContext,
|
||||
savedObjectsClient,
|
||||
rulesClient,
|
||||
|
|
|
@ -50,6 +50,7 @@ export const registerSiemRuleMigrationsInstallTranslatedRoute = (
|
|||
|
||||
await installTranslated({
|
||||
migrationId,
|
||||
enabled: false,
|
||||
securitySolutionContext,
|
||||
savedObjectsClient,
|
||||
rulesClient,
|
||||
|
|
|
@ -27,6 +27,7 @@ import {
|
|||
|
||||
const installPrebuiltRules = async (
|
||||
rulesToInstall: StoredRuleMigration[],
|
||||
enabled: boolean,
|
||||
securitySolutionContext: SecuritySolutionApiRequestHandlerContext,
|
||||
rulesClient: RulesClient,
|
||||
savedObjectsClient: SavedObjectsClientContract,
|
||||
|
@ -41,7 +42,7 @@ const installPrebuiltRules = async (
|
|||
if (item.current) {
|
||||
acc.installed.push(item.current);
|
||||
} else {
|
||||
acc.installable.push(item.target);
|
||||
acc.installable.push({ ...item.target, enabled });
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
|
@ -85,6 +86,7 @@ const installPrebuiltRules = async (
|
|||
|
||||
export const installCustomRules = async (
|
||||
rulesToInstall: StoredRuleMigration[],
|
||||
enabled: boolean,
|
||||
detectionRulesClient: IDetectionRulesClient,
|
||||
logger: Logger
|
||||
): Promise<UpdateRuleMigrationInput[]> => {
|
||||
|
@ -96,8 +98,11 @@ export const installCustomRules = async (
|
|||
if (!isMigrationCustomRule(rule.elastic_rule)) {
|
||||
return;
|
||||
}
|
||||
const payloadRule = convertMigrationCustomRuleToSecurityRulePayload(rule.elastic_rule);
|
||||
const createdRule = await detectionRulesClient.createPrebuiltRule({
|
||||
const payloadRule = convertMigrationCustomRuleToSecurityRulePayload(
|
||||
rule.elastic_rule,
|
||||
enabled
|
||||
);
|
||||
const createdRule = await detectionRulesClient.createCustomRule({
|
||||
params: payloadRule,
|
||||
});
|
||||
rulesToUpdate.push({
|
||||
|
@ -131,6 +136,11 @@ interface InstallTranslatedProps {
|
|||
*/
|
||||
ids?: string[];
|
||||
|
||||
/**
|
||||
* Indicates whether the installed migration rules should be enabled
|
||||
*/
|
||||
enabled: boolean;
|
||||
|
||||
/**
|
||||
* The security solution context
|
||||
*/
|
||||
|
@ -155,6 +165,7 @@ interface InstallTranslatedProps {
|
|||
export const installTranslated = async ({
|
||||
migrationId,
|
||||
ids,
|
||||
enabled,
|
||||
securitySolutionContext,
|
||||
rulesClient,
|
||||
savedObjectsClient,
|
||||
|
@ -186,6 +197,7 @@ export const installTranslated = async ({
|
|||
|
||||
const updatedPrebuiltRules = await installPrebuiltRules(
|
||||
prebuiltRulesToInstall,
|
||||
enabled,
|
||||
securitySolutionContext,
|
||||
rulesClient,
|
||||
savedObjectsClient,
|
||||
|
@ -194,6 +206,7 @@ export const installTranslated = async ({
|
|||
|
||||
const updatedCustomRules = await installCustomRules(
|
||||
customRulesToInstall,
|
||||
enabled,
|
||||
detectionRulesClient,
|
||||
logger
|
||||
);
|
||||
|
|
|
@ -15,10 +15,7 @@ import type {
|
|||
QueryDslQueryContainer,
|
||||
} from '@elastic/elasticsearch/lib/api/types';
|
||||
import type { StoredRuleMigration } from '../types';
|
||||
import {
|
||||
SiemMigrationRuleTranslationResult,
|
||||
SiemMigrationStatus,
|
||||
} from '../../../../../common/siem_migrations/constants';
|
||||
import { SiemMigrationStatus } from '../../../../../common/siem_migrations/constants';
|
||||
import type {
|
||||
ElasticRule,
|
||||
RuleMigration,
|
||||
|
@ -26,6 +23,8 @@ import type {
|
|||
RuleMigrationTranslationStats,
|
||||
} from '../../../../../common/siem_migrations/model/rule_migration.gen';
|
||||
import { RuleMigrationsDataBaseClient } from './rule_migrations_data_base_client';
|
||||
import { getSortingOptions, type RuleMigrationSort } from './sort';
|
||||
import { conditions as searchConditions } from './search';
|
||||
|
||||
export type CreateRuleMigrationInput = Omit<
|
||||
RuleMigration,
|
||||
|
@ -47,6 +46,7 @@ export interface RuleMigrationFilters {
|
|||
}
|
||||
export interface RuleMigrationGetOptions {
|
||||
filters?: RuleMigrationFilters;
|
||||
sort?: RuleMigrationSort;
|
||||
from?: number;
|
||||
size?: number;
|
||||
}
|
||||
|
@ -120,13 +120,19 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient
|
|||
/** Retrieves an array of rule documents of a specific migrations */
|
||||
async get(
|
||||
migrationId: string,
|
||||
{ filters = {}, from, size }: RuleMigrationGetOptions = {}
|
||||
{ filters = {}, sort = {}, from, size }: RuleMigrationGetOptions = {}
|
||||
): Promise<{ total: number; data: StoredRuleMigration[] }> {
|
||||
const index = await this.getIndexName();
|
||||
const query = this.getFilterQuery(migrationId, { ...filters });
|
||||
|
||||
const result = await this.esClient
|
||||
.search<RuleMigration>({ index, query, sort: '_doc', from, size })
|
||||
.search<RuleMigration>({
|
||||
index,
|
||||
query,
|
||||
sort: sort.sortField ? getSortingOptions(sort) : undefined,
|
||||
from,
|
||||
size,
|
||||
})
|
||||
.catch((error) => {
|
||||
this.logger.error(`Error searching rule migrations: ${error.message}`);
|
||||
throw error;
|
||||
|
@ -238,8 +244,8 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient
|
|||
const query = this.getFilterQuery(migrationId);
|
||||
|
||||
const aggregations = {
|
||||
prebuilt: { filter: conditions.isPrebuilt() },
|
||||
installable: { filter: { bool: { must: conditions.isInstallable() } } },
|
||||
prebuilt: { filter: searchConditions.isPrebuilt() },
|
||||
installable: { filter: { bool: { must: searchConditions.isInstallable() } } },
|
||||
};
|
||||
const result = await this.esClient
|
||||
.search({ index, query, aggregations, _source: false })
|
||||
|
@ -351,47 +357,14 @@ export class RuleMigrationsDataRulesClient extends RuleMigrationsDataBaseClient
|
|||
filter.push({ terms: { _id: ids } });
|
||||
}
|
||||
if (installable) {
|
||||
filter.push(...conditions.isInstallable());
|
||||
filter.push(...searchConditions.isInstallable());
|
||||
}
|
||||
if (prebuilt) {
|
||||
filter.push(conditions.isPrebuilt());
|
||||
filter.push(searchConditions.isPrebuilt());
|
||||
}
|
||||
if (searchTerm?.length) {
|
||||
filter.push(conditions.matchTitle(searchTerm));
|
||||
filter.push(searchConditions.matchTitle(searchTerm));
|
||||
}
|
||||
return { bool: { filter } };
|
||||
}
|
||||
}
|
||||
|
||||
const conditions = {
|
||||
isFullyTranslated(): QueryDslQueryContainer {
|
||||
return { term: { translation_result: SiemMigrationRuleTranslationResult.FULL } };
|
||||
},
|
||||
isNotInstalled(): QueryDslQueryContainer {
|
||||
return {
|
||||
nested: {
|
||||
path: 'elastic_rule',
|
||||
query: { bool: { must_not: { exists: { field: 'elastic_rule.id' } } } },
|
||||
},
|
||||
};
|
||||
},
|
||||
isPrebuilt(): QueryDslQueryContainer {
|
||||
return {
|
||||
nested: {
|
||||
path: 'elastic_rule',
|
||||
query: { exists: { field: 'elastic_rule.prebuilt_rule_id' } },
|
||||
},
|
||||
};
|
||||
},
|
||||
matchTitle(title: string): QueryDslQueryContainer {
|
||||
return {
|
||||
nested: {
|
||||
path: 'elastic_rule',
|
||||
query: { match: { 'elastic_rule.title': title } },
|
||||
},
|
||||
};
|
||||
},
|
||||
isInstallable(): QueryDslQueryContainer[] {
|
||||
return [this.isFullyTranslated(), this.isNotInstalled()];
|
||||
},
|
||||
};
|
||||
|
|
|
@ -19,14 +19,14 @@ export const ruleMigrationsFieldMap: FieldMap<SchemaFieldMapKeys<Omit<RuleMigrat
|
|||
original_rule: { type: 'nested', required: true },
|
||||
'original_rule.vendor': { type: 'keyword', required: true },
|
||||
'original_rule.id': { type: 'keyword', required: true },
|
||||
'original_rule.title': { type: 'text', required: true },
|
||||
'original_rule.title': { type: 'text', required: true, fields: { keyword: { type: 'keyword' } } },
|
||||
'original_rule.description': { type: 'text', required: false },
|
||||
'original_rule.query': { type: 'text', required: true },
|
||||
'original_rule.query_language': { type: 'keyword', required: true },
|
||||
'original_rule.annotations': { type: 'nested', required: false },
|
||||
'original_rule.annotations.mitre_attack': { type: 'keyword', array: true, required: false },
|
||||
elastic_rule: { type: 'nested', required: false },
|
||||
'elastic_rule.title': { type: 'text', required: true },
|
||||
'elastic_rule.title': { type: 'text', required: true, fields: { keyword: { type: 'keyword' } } },
|
||||
'elastic_rule.integration_ids': { type: 'keyword', array: true, required: false },
|
||||
'elastic_rule.query': { type: 'text', required: true },
|
||||
'elastic_rule.query_language': { type: 'keyword', required: true },
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* 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 { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { SiemMigrationRuleTranslationResult } from '../../../../../common/siem_migrations/constants';
|
||||
|
||||
export const conditions = {
|
||||
isFullyTranslated(): QueryDslQueryContainer {
|
||||
return { term: { translation_result: SiemMigrationRuleTranslationResult.FULL } };
|
||||
},
|
||||
isNotInstalled(): QueryDslQueryContainer {
|
||||
return {
|
||||
nested: {
|
||||
path: 'elastic_rule',
|
||||
query: { bool: { must_not: { exists: { field: 'elastic_rule.id' } } } },
|
||||
},
|
||||
};
|
||||
},
|
||||
isPrebuilt(): QueryDslQueryContainer {
|
||||
return {
|
||||
nested: {
|
||||
path: 'elastic_rule',
|
||||
query: { exists: { field: 'elastic_rule.prebuilt_rule_id' } },
|
||||
},
|
||||
};
|
||||
},
|
||||
matchTitle(title: string): QueryDslQueryContainer {
|
||||
return {
|
||||
nested: {
|
||||
path: 'elastic_rule',
|
||||
query: { match: { 'elastic_rule.title': title } },
|
||||
},
|
||||
};
|
||||
},
|
||||
isInstallable(): QueryDslQueryContainer[] {
|
||||
return [this.isFullyTranslated(), this.isNotInstalled()];
|
||||
},
|
||||
};
|
|
@ -0,0 +1,127 @@
|
|||
/*
|
||||
* 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 * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
|
||||
export interface RuleMigrationSort {
|
||||
sortField?: string;
|
||||
sortDirection?: estypes.SortOrder;
|
||||
}
|
||||
|
||||
const sortMissingValue = (direction: estypes.SortOrder = 'asc') =>
|
||||
direction === 'desc' ? '_last' : '_first';
|
||||
|
||||
const sortingOptions = {
|
||||
matchedPrebuiltRule(direction: estypes.SortOrder = 'asc'): estypes.SortCombinations[] {
|
||||
return [
|
||||
{
|
||||
'elastic_rule.prebuilt_rule_id': {
|
||||
order: direction,
|
||||
nested: { path: 'elastic_rule' },
|
||||
missing: sortMissingValue(direction),
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
severity(direction: estypes.SortOrder = 'asc'): estypes.SortCombinations[] {
|
||||
const field = 'elastic_rule.severity';
|
||||
return [
|
||||
{
|
||||
_script: {
|
||||
order: direction,
|
||||
type: 'number',
|
||||
script: {
|
||||
source: `
|
||||
if (doc.containsKey('${field}') && !doc['${field}'].empty) {
|
||||
def value = doc['${field}'].value.toLowerCase();
|
||||
if (value == 'critical') { return 3 }
|
||||
if (value == 'high') { return 2 }
|
||||
if (value == 'medium') { return 1 }
|
||||
if (value == 'low') { return 0 }
|
||||
}
|
||||
return -1;
|
||||
`,
|
||||
lang: 'painless',
|
||||
},
|
||||
nested: { path: 'elastic_rule' },
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
status(direction: estypes.SortOrder = 'asc'): estypes.SortCombinations[] {
|
||||
const field = 'translation_result';
|
||||
const installedRuleField = 'elastic_rule.id';
|
||||
return [
|
||||
{
|
||||
_script: {
|
||||
order: direction,
|
||||
type: 'number',
|
||||
script: {
|
||||
source: `
|
||||
if (doc.containsKey('${field}') && !doc['${field}'].empty) {
|
||||
def value = doc['${field}'].value.toLowerCase();
|
||||
if (value == 'full') { return 2 }
|
||||
if (value == 'partial') { return 1 }
|
||||
if (value == 'untranslatable') { return 0 }
|
||||
}
|
||||
return -1;
|
||||
`,
|
||||
lang: 'painless',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
_script: {
|
||||
order: direction,
|
||||
type: 'number',
|
||||
script: {
|
||||
source: `
|
||||
if (doc.containsKey('${installedRuleField}') && !doc['${installedRuleField}'].empty) {
|
||||
return 0;
|
||||
}
|
||||
return -1;
|
||||
`,
|
||||
lang: 'painless',
|
||||
},
|
||||
nested: { path: 'elastic_rule' },
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
updated(direction: estypes.SortOrder = 'asc'): estypes.SortCombinations[] {
|
||||
return [{ updated_at: direction }];
|
||||
},
|
||||
name(direction: estypes.SortOrder = 'asc'): estypes.SortCombinations[] {
|
||||
return [
|
||||
{ 'elastic_rule.title.keyword': { order: direction, nested: { path: 'elastic_rule' } } },
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
const DEFAULT_SORTING: estypes.Sort = [
|
||||
...sortingOptions.status('desc'),
|
||||
...sortingOptions.matchedPrebuiltRule('desc'),
|
||||
...sortingOptions.severity(),
|
||||
...sortingOptions.updated(),
|
||||
];
|
||||
|
||||
const sortingOptionsMap: {
|
||||
[key: string]: (direction?: estypes.SortOrder) => estypes.SortCombinations[];
|
||||
} = {
|
||||
'elastic_rule.title': sortingOptions.name,
|
||||
'elastic_rule.severity': sortingOptions.severity,
|
||||
'elastic_rule.prebuilt_rule_id': sortingOptions.matchedPrebuiltRule,
|
||||
translation_result: sortingOptions.status,
|
||||
updated_at: sortingOptions.updated,
|
||||
};
|
||||
|
||||
export const getSortingOptions = (sort?: RuleMigrationSort): estypes.Sort => {
|
||||
if (!sort?.sortField) {
|
||||
return DEFAULT_SORTING;
|
||||
}
|
||||
return sortingOptionsMap[sort.sortField]?.(sort.sortDirection) ?? DEFAULT_SORTING;
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue