[Security Solution] ThreeWayDiff UI: Add FieldReadOnly component (#191499)

**Partially addresses: https://github.com/elastic/kibana/issues/171520**
**Follow-up PR: https://github.com/elastic/kibana/pull/192342**

This is the 1st of the 2 PRs for `FieldReadOnly`. The second PR will add
more field components. I split the work into two PRs to keep the number
of changed files reasonable.

## Summary

This PR adds the `FieldReadOnly` component along with some field
components. Field components display a read-only view of a particular
`DiffableRule` field, similar to how fields are shown on the Rule
Details page.

`FieldReadOnly` and field components will be displayed in the right side
of the new Diff tab of the Upgrade flyout (see it on the [Miro
board](https://miro.com/app/board/uXjVK0gqjjQ=/?moveToWidget=3458764594148126123&cot=14)).
They will let the user see how an upgraded version of a rule will look
like in a user-friendly way.


### Running
`FinalReadOnly` and its field components are not yet integrated into the
flyout, but you can view components in Storybook.
1. Run Storybook: `yarn storybook security_solution`
2. Go to `http://localhost:9001` in browser.

<img width="1062" alt="Scherm­afbeelding 2024-09-03 om 13 05 11"
src="https://github.com/user-attachments/assets/13b227d4-1321-47d9-a0a7-93868c9f4a15">

## Changes
- `FieldReadOnly` component itself was added. It shows a field component
based on a `fieldName` prop.
- Field components (like `DataSourceReadOnly`) were added. These
components mostly import and reuse components from the Rule Details
page.
- Each field component has a Storybook story. I had to mock dependencies
for some field components to make them work in Storybook.
- Rule Details page and Overview tab of the flyout now display query
language for Custom Query, Saved Query and Indicator Match rules.
Language can be either KQL or Lucene. Since language will be displayed
in the new Diff tab, it makes sense to show it in other places as well
to keep it consistent.
This commit is contained in:
Nikita Indik 2024-09-09 10:55:19 +02:00 committed by GitHub
parent fa686b9905
commit e308d17195
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
39 changed files with 1950 additions and 46 deletions

View file

@ -21,7 +21,7 @@ export interface RuleUpgradeStatsForReview {
/** Number of installed prebuilt rules available for upgrade (stock + customized) */
num_rules_to_upgrade_total: number;
/** Number of installed prebuilt rules with upgrade conflicts (SOLVABLE or NON_SOLVALBE) */
/** Number of installed prebuilt rules with upgrade conflicts (SOLVABLE or NON_SOLVABLE) */
num_rules_with_conflicts: number;
/** Number of installed prebuilt rules with NON_SOLVABLE upgrade conflicts */

View file

@ -5,8 +5,13 @@
* 2.0.
*/
import type { RuleFieldsDiff, ThreeWayDiff } from '../../../../../common/api/detection_engine';
import { ThreeWayDiffOutcome } from '../../../../../common/api/detection_engine';
import type { Filter } from '@kbn/es-query';
import type {
DiffableAllFields,
RuleFieldsDiff,
ThreeWayDiff,
} from '../../../../../common/api/detection_engine';
import { DataSourceType, ThreeWayDiffOutcome } from '../../../../../common/api/detection_engine';
import type { FieldsGroupDiff } from '../../model/rule_details/rule_field_diff';
import {
ABOUT_UPGRADE_FIELD_ORDER,
@ -14,6 +19,8 @@ import {
SCHEDULE_UPGRADE_FIELD_ORDER,
SETUP_UPGRADE_FIELD_ORDER,
} from './constants';
import * as i18n from './translations';
import { assertUnreachable } from '../../../../../common/utility_types';
export const getSectionedFieldDiffs = (fields: FieldsGroupDiff[]) => {
const aboutFields = [];
@ -57,3 +64,58 @@ export const filterUnsupportedDiffOutcomes = (
);
})
);
export function getQueryLanguageLabel(language: string) {
switch (language) {
case 'kuery':
return i18n.KUERY_LANGUAGE_LABEL;
case 'lucene':
return i18n.LUCENE_LANGUAGE_LABEL;
default:
return language;
}
}
/**
* Assigns type `Filter` to items that have a `meta` property. Removes any other items.
*/
export function typeCheckFilters(filters: unknown[]): Filter[] {
return filters.filter((f) => {
if (typeof f === 'object' && f !== null && 'meta' in f) {
return true;
}
return false;
}) as Filter[];
}
type DataSourceProps =
| {
index: undefined;
dataViewId: undefined;
}
| {
index: string[];
dataViewId: undefined;
}
| {
index: undefined;
dataViewId: string;
};
/**
* Extracts `index` and `dataViewId` from a `data_source` object for use in the `Filters` component.
*/
export function getDataSourceProps(dataSource: DiffableAllFields['data_source']): DataSourceProps {
if (!dataSource) {
return { index: undefined, dataViewId: undefined };
}
if (dataSource.type === DataSourceType.index_patterns) {
return { index: dataSource.index_patterns, dataViewId: undefined };
} else if (dataSource.type === DataSourceType.data_view) {
return { index: undefined, dataViewId: dataSource.data_view_id };
}
return assertUnreachable(dataSource);
}

View file

@ -43,10 +43,10 @@ const OverrideColumn = styled(EuiFlexItem)`
text-overflow: ellipsis;
`;
const OverrideValueColumn = styled(EuiFlexItem)`
width: 30px;
max-width: 30px;
const OverrideValueColumn = styled.div`
width: 50px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
`;
@ -86,7 +86,7 @@ interface SeverityMappingItemProps {
severityMappingItem: SeverityMappingItemType;
}
const SeverityMappingItem = ({ severityMappingItem }: SeverityMappingItemProps) => (
export const SeverityMappingItem = ({ severityMappingItem }: SeverityMappingItemProps) => (
<EuiFlexGroup alignItems="center" gutterSize="s">
<OverrideColumn>
<EuiToolTip
@ -96,16 +96,18 @@ const SeverityMappingItem = ({ severityMappingItem }: SeverityMappingItemProps)
<span data-test-subj="severityOverrideField">{`${severityMappingItem.field}:`}</span>
</EuiToolTip>
</OverrideColumn>
<OverrideValueColumn>
<EuiFlexItem grow={false}>
<EuiToolTip
content={severityMappingItem.value}
data-test-subj={`severityOverrideValue-${severityMappingItem.value}`}
>
<span data-test-subj="severityOverrideValue">
{defaultToEmptyTag(severityMappingItem.value)}
</span>
<OverrideValueColumn>
<span data-test-subj="severityOverrideValue">
{defaultToEmptyTag(severityMappingItem.value)}
</span>
</OverrideValueColumn>
</EuiToolTip>
</OverrideValueColumn>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiIcon type={'sortRight'} />
</EuiFlexItem>
@ -132,7 +134,7 @@ interface RiskScoreMappingItemProps {
riskScoreMappingItem: RiskScoreMappingItemType;
}
const RiskScoreMappingItem = ({ riskScoreMappingItem }: RiskScoreMappingItemProps) => (
export const RiskScoreMappingItem = ({ riskScoreMappingItem }: RiskScoreMappingItemProps) => (
<EuiFlexGroup alignItems="center" gutterSize="s">
<OverrideColumn>
<EuiToolTip
@ -218,7 +220,7 @@ interface ThreatProps {
threat: Threats;
}
const Threat = ({ threat }: ThreatProps) => (
export const Threat = ({ threat }: ThreatProps) => (
<ThreatEuiFlexGroup threat={filterEmptyThreats(threat)} data-test-subj="threatPropertyValue" />
);
@ -226,7 +228,7 @@ interface ThreatIndicatorPathProps {
threatIndicatorPath: string;
}
const ThreatIndicatorPath = ({ threatIndicatorPath }: ThreatIndicatorPathProps) => (
export const ThreatIndicatorPath = ({ threatIndicatorPath }: ThreatIndicatorPathProps) => (
<EuiText size="s">{threatIndicatorPath}</EuiText>
);

View file

@ -57,12 +57,14 @@ import {
queryStyles,
useRequiredFieldsStyles,
} from './rule_definition_section.styles';
import { getQueryLanguageLabel } from './helpers';
import { useDefaultIndexPattern } from './use_default_index_pattern';
interface SavedQueryNameProps {
savedQueryName: string;
}
const SavedQueryName = ({ savedQueryName }: SavedQueryNameProps) => (
export const SavedQueryName = ({ savedQueryName }: SavedQueryNameProps) => (
<EuiText size="s" data-test-subj="savedQueryNamePropertyValue">
{savedQueryName}
</EuiText>
@ -75,12 +77,19 @@ interface FiltersProps {
'data-test-subj'?: string;
}
const Filters = ({ filters, dataViewId, index, 'data-test-subj': dataTestSubj }: FiltersProps) => {
export const Filters = ({
filters,
dataViewId,
index,
'data-test-subj': dataTestSubj,
}: FiltersProps) => {
const flattenedFilters = mapAndFlattenFilters(filters);
const defaultIndexPattern = useDefaultIndexPattern();
const { indexPattern } = useRuleIndexPattern({
dataSourceType: dataViewId ? DataSourceType.DataView : DataSourceType.IndexPatterns,
index: index ?? [],
index: index ?? defaultIndexPattern,
dataViewId,
});
@ -104,7 +113,7 @@ interface QueryProps {
'data-test-subj'?: string;
}
const Query = ({ query, 'data-test-subj': dataTestSubj = 'query' }: QueryProps) => {
export const Query = ({ query, 'data-test-subj': dataTestSubj = 'query' }: QueryProps) => {
const styles = queryStyles;
return (
<div data-test-subj={dataTestSubj} className={styles.content}>
@ -117,7 +126,7 @@ interface IndexProps {
index: string[];
}
const Index = ({ index }: IndexProps) => (
export const Index = ({ index }: IndexProps) => (
<BadgeList badges={index} data-test-subj="indexPropertyValue" />
);
@ -125,7 +134,7 @@ interface DataViewIdProps {
dataViewId: string;
}
const DataViewId = ({ dataViewId }: DataViewIdProps) => (
export const DataViewId = ({ dataViewId }: DataViewIdProps) => (
<EuiText size="s" data-test-subj="dataViewIdPropertyValue">
{dataViewId}
</EuiText>
@ -135,7 +144,7 @@ interface DataViewIndexPatternProps {
dataViewId: string;
}
const DataViewIndexPattern = ({ dataViewId }: DataViewIndexPatternProps) => {
export const DataViewIndexPattern = ({ dataViewId }: DataViewIndexPatternProps) => {
const { data } = useKibana().services;
const [indexPattern, setIndexPattern] = React.useState('');
const [hasError, setHasError] = React.useState(false);
@ -191,18 +200,24 @@ const AnomalyThreshold = ({ anomalyThreshold }: AnomalyThresholdProps) => (
);
interface MachineLearningJobListProps {
jobIds: string[];
jobIds?: string | string[];
isInteractive: boolean;
}
const MachineLearningJobList = ({ jobIds, isInteractive }: MachineLearningJobListProps) => {
export const MachineLearningJobList = ({ jobIds, isInteractive }: MachineLearningJobListProps) => {
const { jobs } = useSecurityJobs();
if (isInteractive) {
return <MlJobsDescription jobIds={jobIds} />;
if (!jobIds) {
return null;
}
const relevantJobs = jobs.filter((job) => jobIds.includes(job.id));
const jobIdsArray = Array.isArray(jobIds) ? jobIds : [jobIds];
if (isInteractive) {
return <MlJobsDescription jobIds={jobIdsArray} />;
}
const relevantJobs = jobs.filter((job) => jobIdsArray.includes(job.id));
return (
<>
@ -251,7 +266,7 @@ interface RequiredFieldsProps {
requiredFields: RequiredFieldArray;
}
const RequiredFields = ({ requiredFields }: RequiredFieldsProps) => {
export const RequiredFields = ({ requiredFields }: RequiredFieldsProps) => {
const styles = useRequiredFieldsStyles();
return (
@ -293,7 +308,7 @@ interface ThreatIndexProps {
threatIndex: string[];
}
const ThreatIndex = ({ threatIndex }: ThreatIndexProps) => (
export const ThreatIndex = ({ threatIndex }: ThreatIndexProps) => (
<BadgeList badges={threatIndex} data-test-subj="threatIndexPropertyValue" />
);
@ -301,7 +316,7 @@ interface ThreatMappingProps {
threatMapping: ThreatMappingType;
}
const ThreatMapping = ({ threatMapping }: ThreatMappingProps) => {
export const ThreatMapping = ({ threatMapping }: ThreatMappingProps) => {
const description = threatMapping.reduce<string>(
(accumThreatMaps, threatMap, threatMapIndex, { length: threatMappingLength }) => {
const matches = threatMap.entries.reduce<string>(
@ -434,14 +449,28 @@ const prepareDefinitionSectionListItems = (
}
if (savedQuery) {
definitionSectionListItems.push({
title: (
<span data-test-subj="savedQueryNamePropertyTitle">
{descriptionStepI18n.SAVED_QUERY_NAME_LABEL}
</span>
),
description: <SavedQueryName savedQueryName={savedQuery.attributes.title} />,
});
definitionSectionListItems.push(
{
title: (
<span data-test-subj="savedQueryNamePropertyTitle">
{descriptionStepI18n.SAVED_QUERY_NAME_LABEL}
</span>
),
description: <SavedQueryName savedQueryName={savedQuery.attributes.title} />,
},
{
title: (
<span data-test-subj="savedQueryLanguagePropertyTitle">
{i18n.SAVED_QUERY_LANGUAGE_LABEL}
</span>
),
description: (
<span data-test-subj="savedQueryLanguagePropertyValue">
{getQueryLanguageLabel(savedQuery.attributes.query.language)}
</span>
),
}
);
if (savedQuery.attributes.filters) {
definitionSectionListItems.push({
@ -452,8 +481,10 @@ const prepareDefinitionSectionListItems = (
),
description: (
<Filters
filters={savedQuery.attributes.filters as Filter[]}
filters={savedQuery.attributes.filters}
data-test-subj="savedQueryFiltersPropertyValue"
dataViewId={'data_view_id' in rule ? rule.data_view_id : undefined}
index={'index' in rule ? rule.index : undefined}
/>
),
});
@ -508,12 +539,26 @@ const prepareDefinitionSectionListItems = (
description: <Query query={rule.query} data-test-subj="esqlQueryPropertyValue" />,
});
} else {
definitionSectionListItems.push({
title: (
<span data-test-subj="customQueryPropertyTitle">{descriptionStepI18n.QUERY_LABEL}</span>
),
description: <Query query={rule.query} data-test-subj="customQueryPropertyValue" />,
});
definitionSectionListItems.push(
{
title: (
<span data-test-subj="customQueryPropertyTitle">{descriptionStepI18n.QUERY_LABEL}</span>
),
description: <Query query={rule.query} data-test-subj="customQueryPropertyValue" />,
},
{
title: (
<span data-test-subj="customQueryLanguagePropertyTitle">
{i18n.QUERY_LANGUAGE_LABEL}
</span>
),
description: (
<span data-test-subj="customQueryLanguagePropertyValue">
{getQueryLanguageLabel(rule.language || '')}
</span>
),
}
);
}
}
@ -542,7 +587,7 @@ const prepareDefinitionSectionListItems = (
),
description: (
<MachineLearningJobList
jobIds={rule.machine_learning_job_id as string[]}
jobIds={rule.machine_learning_job_id}
isInteractive={isInteractive}
/>
),
@ -633,6 +678,21 @@ const prepareDefinitionSectionListItems = (
});
}
if ('threat_language' in rule && rule.threat_language) {
definitionSectionListItems.push({
title: (
<span data-test-subj="threatQueryLanguagePropertyTitle">
{i18n.THREAT_QUERY_LANGUAGE_LABEL}
</span>
),
description: (
<span data-test-subj="threatQueryLanguagePropertyValue">
{getQueryLanguageLabel(rule.threat_language)}
</span>
),
});
}
if ('new_terms_fields' in rule && rule.new_terms_fields && rule.new_terms_fields.length > 0) {
definitionSectionListItems.push({
title: (

View file

@ -0,0 +1,89 @@
/*
* 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 type { DiffableAllFields } from '../../../../../../../common/api/detection_engine';
import { KqlQueryReadOnly } from './fields/kql_query';
import { DataSourceReadOnly } from './fields/data_source/data_source';
import { EqlQueryReadOnly } from './fields/eql_query/eql_query';
import { EsqlQueryReadOnly } from './fields/esql_query/esql_query';
import { MachineLearningJobIdReadOnly } from './fields/machine_learning_job_id/machine_learning_job_id';
import { RelatedIntegrationsReadOnly } from './fields/related_integrations/related_integrations';
import { RequiredFieldsReadOnly } from './fields/required_fields/required_fields';
import { SeverityMappingReadOnly } from './fields/severity_mapping/severity_mapping';
import { RiskScoreMappingReadOnly } from './fields/risk_score_mapping/risk_score_mapping';
import { ThreatMappingReadOnly } from './fields/threat_mapping/threat_mapping';
import { ThreatReadOnly } from './fields/threat/threat';
import { ThreatIndexReadOnly } from './fields/threat_index/threat_index';
import { ThreatIndicatorPathReadOnly } from './fields/threat_indicator_path/threat_indicator_path';
import { ThreatQueryReadOnly } from './fields/threat_query/threat_query';
interface FieldReadOnlyProps {
fieldName: keyof DiffableAllFields;
finalDiffableRule: DiffableAllFields;
}
export function FieldReadOnly({ fieldName, finalDiffableRule }: FieldReadOnlyProps) {
switch (fieldName) {
case 'data_source':
return <DataSourceReadOnly dataSource={finalDiffableRule.data_source} />;
case 'eql_query':
return (
<EqlQueryReadOnly
eqlQuery={finalDiffableRule.eql_query}
dataSource={finalDiffableRule.data_source}
/>
);
case 'esql_query':
return <EsqlQueryReadOnly esqlQuery={finalDiffableRule.esql_query} />;
case 'kql_query':
return (
<KqlQueryReadOnly
kqlQuery={finalDiffableRule.kql_query}
dataSource={finalDiffableRule.data_source}
ruleType={finalDiffableRule.type}
/>
);
case 'machine_learning_job_id':
return (
<MachineLearningJobIdReadOnly
machineLearningJobId={finalDiffableRule.machine_learning_job_id}
/>
);
case 'related_integrations':
return (
<RelatedIntegrationsReadOnly relatedIntegrations={finalDiffableRule.related_integrations} />
);
case 'required_fields':
return <RequiredFieldsReadOnly requiredFields={finalDiffableRule.required_fields} />;
case 'risk_score_mapping':
return <RiskScoreMappingReadOnly riskScoreMapping={finalDiffableRule.risk_score_mapping} />;
case 'severity_mapping':
return <SeverityMappingReadOnly severityMapping={finalDiffableRule.severity_mapping} />;
case 'threat':
return <ThreatReadOnly threat={finalDiffableRule.threat} />;
case 'threat_index':
return <ThreatIndexReadOnly threatIndex={finalDiffableRule.threat_index} />;
case 'threat_indicator_path':
return (
<ThreatIndicatorPathReadOnly
threatIndicatorPath={finalDiffableRule.threat_indicator_path}
/>
);
case 'threat_mapping':
return <ThreatMappingReadOnly threatMapping={finalDiffableRule.threat_mapping} />;
case 'threat_query':
return (
<ThreatQueryReadOnly
threatQuery={finalDiffableRule.threat_query}
dataSource={finalDiffableRule.data_source}
/>
);
default:
return null;
}
}

View file

@ -0,0 +1,61 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import type { Story } from '@storybook/react';
import { FieldReadOnly } from '../../field_readonly';
import type { DiffableAllFields } from '../../../../../../../../../common/api/detection_engine';
import { ThreeWayDiffStorybookProviders } from '../../storybook/three_way_diff_storybook_providers';
import {
dataSourceWithDataView,
dataSourceWithIndexPatterns,
mockDataView,
} from '../../storybook/mocks';
export default {
component: FieldReadOnly,
title: 'Rule Management/Prebuilt Rules/Upgrade Flyout/ThreeWayDiff/FieldReadOnly/data_source',
};
interface TemplateProps {
finalDiffableRule: Partial<DiffableAllFields>;
kibanaServicesMock?: Record<string, unknown>;
}
const Template: Story<TemplateProps> = (args) => {
return (
<ThreeWayDiffStorybookProviders kibanaServicesMock={args.kibanaServicesMock}>
<FieldReadOnly
fieldName="data_source"
finalDiffableRule={args.finalDiffableRule as DiffableAllFields}
/>
</ThreeWayDiffStorybookProviders>
);
};
export const DataSourceWithIndexPatterns = Template.bind({});
DataSourceWithIndexPatterns.args = {
finalDiffableRule: {
data_source: dataSourceWithIndexPatterns,
},
};
export const DataSourceWithDataView = Template.bind({});
DataSourceWithDataView.args = {
finalDiffableRule: {
data_source: dataSourceWithDataView,
},
kibanaServicesMock: {
data: {
dataViews: {
get: async () => mockDataView(),
},
},
},
};

View file

@ -0,0 +1,56 @@
/*
* 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 { EuiDescriptionList } from '@elastic/eui';
import { DataSourceType } from '../../../../../../../../../common/api/detection_engine';
import type { DiffableAllFields } from '../../../../../../../../../common/api/detection_engine';
import { Index, DataViewId, DataViewIndexPattern } from '../../../../rule_definition_section';
import * as ruleDetailsI18n from '../../../../translations';
import { assertUnreachable } from '../../../../../../../../../common/utility_types';
interface DataSourceReadOnlyProps {
dataSource: DiffableAllFields['data_source'];
}
export function DataSourceReadOnly({ dataSource }: DataSourceReadOnlyProps) {
if (!dataSource) {
return null;
}
if (dataSource.type === DataSourceType.index_patterns) {
return (
<EuiDescriptionList
listItems={[
{
title: ruleDetailsI18n.INDEX_FIELD_LABEL,
description: <Index index={dataSource.index_patterns} />,
},
]}
/>
);
}
if (dataSource.type === DataSourceType.data_view) {
return (
<EuiDescriptionList
listItems={[
{
title: ruleDetailsI18n.DATA_VIEW_ID_FIELD_LABEL,
description: <DataViewId dataViewId={dataSource.data_view_id} />,
},
{
title: ruleDetailsI18n.DATA_VIEW_INDEX_PATTERN_FIELD_LABEL,
description: <DataViewIndexPattern dataViewId={dataSource.data_view_id} />,
},
]}
/>
);
}
return assertUnreachable(dataSource);
}

View file

@ -0,0 +1,72 @@
/*
* 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 type { Story } from '@storybook/react';
import { FieldReadOnly } from '../../field_readonly';
import type { DiffableAllFields } from '../../../../../../../../../common/api/detection_engine';
import { ThreeWayDiffStorybookProviders } from '../../storybook/three_way_diff_storybook_providers';
import { EqlQueryReadOnly } from './eql_query';
import {
dataSourceWithDataView,
dataSourceWithIndexPatterns,
eqlQuery,
mockDataView,
} from '../../storybook/mocks';
export default {
component: EqlQueryReadOnly,
title: 'Rule Management/Prebuilt Rules/Upgrade Flyout/ThreeWayDiff/FieldReadOnly/eql_query',
};
interface TemplateProps {
finalDiffableRule: Partial<DiffableAllFields>;
kibanaServicesMock?: Record<string, unknown>;
}
const Template: Story<TemplateProps> = (args) => {
return (
<ThreeWayDiffStorybookProviders kibanaServicesMock={args.kibanaServicesMock}>
<FieldReadOnly
fieldName="eql_query"
finalDiffableRule={args.finalDiffableRule as DiffableAllFields}
/>
</ThreeWayDiffStorybookProviders>
);
};
export const EqlQueryWithIndexPatterns = Template.bind({});
EqlQueryWithIndexPatterns.args = {
finalDiffableRule: {
eql_query: eqlQuery,
data_source: dataSourceWithIndexPatterns,
},
kibanaServicesMock: {
data: {
dataViews: {
create: async () => mockDataView(),
},
},
},
};
export const EqlQueryWithDataView = Template.bind({});
EqlQueryWithDataView.args = {
finalDiffableRule: {
eql_query: eqlQuery,
data_source: dataSourceWithDataView,
},
kibanaServicesMock: {
data: {
dataViews: {
get: async () => mockDataView(),
},
},
},
};

View file

@ -0,0 +1,41 @@
/*
* 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 { EuiDescriptionList } from '@elastic/eui';
import type { EuiDescriptionListProps } from '@elastic/eui';
import type { DiffableAllFields } from '../../../../../../../../../common/api/detection_engine';
import * as descriptionStepI18n from '../../../../../../../rule_creation_ui/components/description_step/translations';
import { Query, Filters } from '../../../../rule_definition_section';
import { getDataSourceProps, typeCheckFilters } from '../../../../helpers';
interface EqlQueryReadOnlyProps {
eqlQuery: DiffableAllFields['eql_query'];
dataSource: DiffableAllFields['data_source'];
}
export function EqlQueryReadOnly({ eqlQuery, dataSource }: EqlQueryReadOnlyProps) {
const listItems: EuiDescriptionListProps['listItems'] = [
{
title: descriptionStepI18n.EQL_QUERY_LABEL,
description: <Query query={eqlQuery.query} />,
},
];
const filters = typeCheckFilters(eqlQuery.filters);
if (filters.length > 0) {
const dataSourceProps = getDataSourceProps(dataSource);
listItems.push({
title: descriptionStepI18n.FILTERS_LABEL,
description: <Filters filters={filters} {...dataSourceProps} />,
});
}
return <EuiDescriptionList listItems={listItems} />;
}

View file

@ -0,0 +1,40 @@
/*
* 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 type { Story } from '@storybook/react';
import { FieldReadOnly } from '../../field_readonly';
import type { DiffableAllFields } from '../../../../../../../../../common/api/detection_engine';
export default {
component: FieldReadOnly,
title: 'Rule Management/Prebuilt Rules/Upgrade Flyout/ThreeWayDiff/FieldReadOnly/esql_query',
};
interface TemplateProps {
finalDiffableRule: Partial<DiffableAllFields>;
}
const Template: Story<TemplateProps> = (args) => {
return (
<FieldReadOnly
fieldName="esql_query"
finalDiffableRule={args.finalDiffableRule as DiffableAllFields}
/>
);
};
export const Default = Template.bind({});
Default.args = {
finalDiffableRule: {
esql_query: {
query: `SELECT user.name, source.ip FROM "logs-*" WHERE event.action = 'user_login' AND event.outcome = 'failure'`,
language: 'esql',
},
},
};

View file

@ -0,0 +1,29 @@
/*
* 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 { EuiDescriptionList } from '@elastic/eui';
import * as descriptionStepI18n from '../../../../../../../rule_creation_ui/components/description_step/translations';
import { Query } from '../../../../rule_definition_section';
import type { DiffableAllFields } from '../../../../../../../../../common/api/detection_engine';
interface EsqlQueryReadonlyProps {
esqlQuery: DiffableAllFields['esql_query'];
}
export function EsqlQueryReadOnly({ esqlQuery }: EsqlQueryReadonlyProps) {
return (
<EuiDescriptionList
listItems={[
{
title: descriptionStepI18n.ESQL_QUERY_LABEL,
description: <Query query={esqlQuery.query} />,
},
]}
/>
);
}

View file

@ -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 React from 'react';
import { KqlQueryType } from '../../../../../../../../../common/api/detection_engine';
import type {
DiffableAllFields,
RuleKqlQuery,
} from '../../../../../../../../../common/api/detection_engine';
import { InlineKqlQueryReadOnly } from './inline_kql_query';
import { SavedKqlQueryReadOnly } from './saved_kql_query';
import { assertUnreachable } from '../../../../../../../../../common/utility_types';
interface KqlQueryReadOnlyProps {
kqlQuery: RuleKqlQuery;
dataSource: DiffableAllFields['data_source'];
ruleType: DiffableAllFields['type'];
}
export function KqlQueryReadOnly({ kqlQuery, dataSource, ruleType }: KqlQueryReadOnlyProps) {
if (kqlQuery.type === KqlQueryType.inline_query) {
return <InlineKqlQueryReadOnly kqlQuery={kqlQuery} dataSource={dataSource} />;
}
if (kqlQuery.type === KqlQueryType.saved_query) {
return (
<SavedKqlQueryReadOnly kqlQuery={kqlQuery} dataSource={dataSource} ruleType={ruleType} />
);
}
return assertUnreachable(kqlQuery);
}

View file

@ -0,0 +1,64 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { EuiDescriptionList } from '@elastic/eui';
import type { EuiDescriptionListProps } from '@elastic/eui';
import type {
DiffableAllFields,
InlineKqlQuery,
} from '../../../../../../../../../common/api/detection_engine';
import { Query, Filters } from '../../../../rule_definition_section';
import * as ruleDetailsI18n from '../../../../translations';
import * as descriptionStepI18n from '../../../../../../../rule_creation_ui/components/description_step/translations';
import { getDataSourceProps, getQueryLanguageLabel, typeCheckFilters } from '../../../../helpers';
const defaultI18nLabels = {
query: descriptionStepI18n.QUERY_LABEL,
language: ruleDetailsI18n.QUERY_LANGUAGE_LABEL,
filters: descriptionStepI18n.FILTERS_LABEL,
};
interface InlineQueryProps {
kqlQuery: InlineKqlQuery;
dataSource?: DiffableAllFields['data_source'];
i18nLabels?: {
query: string;
language: string;
filters: string;
};
}
export function InlineKqlQueryReadOnly({
kqlQuery,
dataSource,
i18nLabels = defaultI18nLabels,
}: InlineQueryProps) {
const listItems: EuiDescriptionListProps['listItems'] = [
{
title: i18nLabels.query,
description: <Query query={kqlQuery.query} />,
},
{
title: i18nLabels.language,
description: getQueryLanguageLabel(kqlQuery.language),
},
];
const filters = typeCheckFilters(kqlQuery.filters);
if (filters.length > 0) {
const dataSourceProps = getDataSourceProps(dataSource);
listItems.push({
title: i18nLabels.filters,
description: <Filters filters={filters} {...dataSourceProps} />,
});
}
return <EuiDescriptionList listItems={listItems} />;
}

View file

@ -0,0 +1,135 @@
/*
* 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 type { Story } from '@storybook/react';
import { FieldReadOnly } from '../../field_readonly';
import type {
DiffableAllFields,
RuleKqlQuery,
} from '../../../../../../../../../common/api/detection_engine';
import { ThreeWayDiffStorybookProviders } from '../../storybook/three_way_diff_storybook_providers';
import {
dataSourceWithDataView,
dataSourceWithIndexPatterns,
inlineKqlQuery,
mockDataView,
savedKqlQuery,
savedQueryResponse,
} from '../../storybook/mocks';
export default {
component: FieldReadOnly,
title: 'Rule Management/Prebuilt Rules/Upgrade Flyout/ThreeWayDiff/FieldReadOnly/kql_query',
};
interface TemplateProps {
finalDiffableRule: Partial<DiffableAllFields> | { kql_query: RuleKqlQuery };
kibanaServicesMock?: Record<string, unknown>;
}
const Template: Story<TemplateProps> = (args) => {
return (
<ThreeWayDiffStorybookProviders kibanaServicesMock={args.kibanaServicesMock}>
<FieldReadOnly
fieldName="kql_query"
finalDiffableRule={args.finalDiffableRule as DiffableAllFields}
/>
</ThreeWayDiffStorybookProviders>
);
};
export const InlineKqlQueryWithIndexPatterns = Template.bind({});
InlineKqlQueryWithIndexPatterns.args = {
finalDiffableRule: {
kql_query: inlineKqlQuery,
data_source: dataSourceWithIndexPatterns,
},
kibanaServicesMock: {
data: {
dataViews: {
create: async () => mockDataView(),
},
},
},
};
export const InlineKqlQueryWithDataView = Template.bind({});
InlineKqlQueryWithDataView.args = {
finalDiffableRule: {
kql_query: inlineKqlQuery,
data_source: dataSourceWithDataView,
},
kibanaServicesMock: {
data: {
dataViews: {
get: async () => mockDataView(),
},
},
},
};
export const InlineKqlQueryWithoutDataSource = Template.bind({});
/*
Filters should still be displayed if no `data_source` is provided.
Component would fall back to the default index pattern in such case.
*/
InlineKqlQueryWithoutDataSource.args = {
finalDiffableRule: {
kql_query: inlineKqlQuery,
},
kibanaServicesMock: {
data: {
dataViews: {
create: async () => mockDataView(),
},
},
},
};
export const SavedKqlQueryWithIndexPatterns = Template.bind({});
SavedKqlQueryWithIndexPatterns.args = {
finalDiffableRule: {
kql_query: savedKqlQuery,
data_source: dataSourceWithIndexPatterns,
type: 'saved_query',
},
kibanaServicesMock: {
data: {
dataViews: {
create: async () => mockDataView(),
},
},
http: {
get: async () => savedQueryResponse,
},
},
};
export const SavedKqlQueryWithDataView = Template.bind({});
SavedKqlQueryWithDataView.args = {
finalDiffableRule: {
kql_query: savedKqlQuery,
data_source: dataSourceWithDataView,
type: 'saved_query',
},
kibanaServicesMock: {
data: {
dataViews: {
get: async () => mockDataView(),
},
},
http: {
get: async () => savedQueryResponse,
},
},
};

View file

@ -0,0 +1,66 @@
/*
* 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 { EuiDescriptionList } from '@elastic/eui';
import type { EuiDescriptionListProps } from '@elastic/eui';
import type {
SavedKqlQuery,
DiffableRule,
DiffableAllFields,
} from '../../../../../../../../../common/api/detection_engine';
import { Query, SavedQueryName, Filters } from '../../../../rule_definition_section';
import * as ruleDetailsI18n from '../../../../translations';
import * as descriptionStepI18n from '../../../../../../../rule_creation_ui/components/description_step/translations';
import { useGetSavedQuery } from '../../../../../../../../detections/pages/detection_engine/rules/use_get_saved_query';
import { getDataSourceProps, getQueryLanguageLabel } from '../../../../helpers';
interface SavedQueryProps {
kqlQuery: SavedKqlQuery;
dataSource?: DiffableAllFields['data_source'];
ruleType: DiffableRule['type'];
}
export function SavedKqlQueryReadOnly({ kqlQuery, dataSource, ruleType }: SavedQueryProps) {
const { savedQuery } = useGetSavedQuery({
savedQueryId: kqlQuery.saved_query_id,
ruleType,
});
if (!savedQuery) {
return null;
}
const listItems: EuiDescriptionListProps['listItems'] = [
{
title: descriptionStepI18n.SAVED_QUERY_NAME_LABEL,
description: <SavedQueryName savedQueryName={savedQuery.attributes.title} />,
},
{
title: ruleDetailsI18n.SAVED_QUERY_LANGUAGE_LABEL,
description: getQueryLanguageLabel(savedQuery.attributes.query.language),
},
];
if (typeof savedQuery.attributes.query.query === 'string') {
listItems.push({
title: descriptionStepI18n.SAVED_QUERY_LABEL,
description: <Query query={savedQuery.attributes.query.query} />,
});
}
if (savedQuery.attributes.filters) {
const dataSourceProps = getDataSourceProps(dataSource);
listItems.push({
title: descriptionStepI18n.SAVED_QUERY_FILTERS_LABEL,
description: <Filters filters={savedQuery.attributes.filters} {...dataSourceProps} />,
});
}
return <EuiDescriptionList listItems={listItems} />;
}

View file

@ -0,0 +1,83 @@
/*
* 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 { useQueryClient } from '@tanstack/react-query';
import type { Story } from '@storybook/react';
import { MachineLearningJobIdReadOnly } from './machine_learning_job_id';
import type { DiffableAllFields } from '../../../../../../../../../common/api/detection_engine';
import { FieldReadOnly } from '../../field_readonly';
import { ThreeWayDiffStorybookProviders } from '../../storybook/three_way_diff_storybook_providers';
import { GET_MODULES_QUERY_KEY } from '../../../../../../../../common/components/ml_popover/hooks/use_fetch_modules_query';
import { GET_RECOGNIZER_QUERY_KEY } from '../../../../../../../../common/components/ml_popover/hooks/use_fetch_recognizer_query';
import { GET_JOBS_SUMMARY_QUERY_KEY } from '../../../../../../../../common/components/ml/hooks/use_fetch_jobs_summary_query';
export default {
component: MachineLearningJobIdReadOnly,
title:
'Rule Management/Prebuilt Rules/Upgrade Flyout/ThreeWayDiff/FieldReadOnly/machine_learning_job_id',
};
const mockedModulesData = [
{
id: 'security_auth',
jobs: [
{
id: 'auth_high_count_logon_events',
config: {
groups: [],
custom_settings: {
security_app_display_name: 'Spike in Logon Events',
},
},
},
],
},
];
const mockedCompatibleModules = [
{
id: 'security_auth',
},
];
function MockMlData({ children }: { children: React.ReactNode }) {
const queryClient = useQueryClient();
queryClient.setQueryData([GET_JOBS_SUMMARY_QUERY_KEY, {}], []);
queryClient.setQueryData([GET_MODULES_QUERY_KEY, {}], mockedModulesData);
queryClient.setQueryData([GET_RECOGNIZER_QUERY_KEY, {}], mockedCompatibleModules);
return <>{children}</>;
}
interface TemplateProps {
finalDiffableRule: Partial<DiffableAllFields>;
}
const Template: Story<TemplateProps> = (args) => {
return (
<ThreeWayDiffStorybookProviders>
<MockMlData>
<FieldReadOnly
fieldName="machine_learning_job_id"
finalDiffableRule={args.finalDiffableRule as DiffableAllFields}
/>
</MockMlData>
</ThreeWayDiffStorybookProviders>
);
};
export const Default = Template.bind({});
Default.args = {
finalDiffableRule: {
machine_learning_job_id: 'auth_high_count_logon_events',
},
};

View file

@ -0,0 +1,31 @@
/*
* 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 { EuiDescriptionList } from '@elastic/eui';
import { MachineLearningJobList } from '../../../../rule_definition_section';
import type { DiffableAllFields } from '../../../../../../../../../common/api/detection_engine';
import * as ruleDetailsI18n from '../../../../translations';
interface MachineLearningJobIdProps {
machineLearningJobId: DiffableAllFields['machine_learning_job_id'];
}
export function MachineLearningJobIdReadOnly({ machineLearningJobId }: MachineLearningJobIdProps) {
return (
<EuiDescriptionList
listItems={[
{
title: ruleDetailsI18n.MACHINE_LEARNING_JOB_ID_FIELD_LABEL,
description: (
<MachineLearningJobList jobIds={machineLearningJobId} isInteractive={false} />
),
},
]}
/>
);
}

View file

@ -0,0 +1,64 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { useQueryClient } from '@tanstack/react-query';
import type { Story } from '@storybook/react';
import { RelatedIntegrationsReadOnly } from './related_integrations';
import { ThreeWayDiffStorybookProviders } from '../../storybook/three_way_diff_storybook_providers';
import { FieldReadOnly } from '../../field_readonly';
import type { DiffableAllFields } from '../../../../../../../../../common/api/detection_engine';
export default {
component: RelatedIntegrationsReadOnly,
title:
'Rule Management/Prebuilt Rules/Upgrade Flyout/ThreeWayDiff/FieldReadOnly/related_integrations',
};
const mockedIntegrationsData = [
{
package_name: 'endpoint',
package_title: 'Elastic Defend',
latest_package_version: '8.15.1',
installed_package_version: '8.16.0-prerelease.1',
is_installed: true,
is_enabled: false,
},
];
function MockRelatedIntegrationsData({ children }: { children: React.ReactNode }) {
const queryClient = useQueryClient();
queryClient.setQueryData(['integrations'], mockedIntegrationsData);
return <>{children}</>;
}
interface TemplateProps {
finalDiffableRule: Partial<DiffableAllFields>;
}
const Template: Story<TemplateProps> = (args) => {
return (
<ThreeWayDiffStorybookProviders>
<MockRelatedIntegrationsData>
<FieldReadOnly
fieldName="related_integrations"
finalDiffableRule={args.finalDiffableRule as DiffableAllFields}
/>
</MockRelatedIntegrationsData>
</ThreeWayDiffStorybookProviders>
);
};
export const Default = Template.bind({});
Default.args = {
finalDiffableRule: {
related_integrations: [{ package: 'endpoint', version: '^8.2.0' }],
},
};

View file

@ -0,0 +1,29 @@
/*
* 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 { EuiDescriptionList } from '@elastic/eui';
import * as ruleDetailsI18n from '../../../../translations';
import { RelatedIntegrationsDescription } from '../../../../../../../../detections/components/rules/related_integrations/integrations_description';
import type { RelatedIntegrationArray } from '../../../../../../../../../common/api/detection_engine';
interface RelatedIntegrationsReadOnly {
relatedIntegrations: RelatedIntegrationArray;
}
export function RelatedIntegrationsReadOnly({ relatedIntegrations }: RelatedIntegrationsReadOnly) {
return (
<EuiDescriptionList
listItems={[
{
title: ruleDetailsI18n.RELATED_INTEGRATIONS_FIELD_LABEL,
description: <RelatedIntegrationsDescription relatedIntegrations={relatedIntegrations} />,
},
]}
/>
);
}

View file

@ -0,0 +1,40 @@
/*
* 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 type { Story } from '@storybook/react';
import { RequiredFieldsReadOnly } from './required_fields';
import { FieldReadOnly } from '../../field_readonly';
import type { DiffableAllFields } from '../../../../../../../../../common/api/detection_engine';
export default {
component: RequiredFieldsReadOnly,
title: 'Rule Management/Prebuilt Rules/Upgrade Flyout/ThreeWayDiff/FieldReadOnly/required_fields',
};
interface TemplateProps {
finalDiffableRule: Partial<DiffableAllFields>;
}
const Template: Story<TemplateProps> = (args) => {
return (
<FieldReadOnly
fieldName="required_fields"
finalDiffableRule={args.finalDiffableRule as DiffableAllFields}
/>
);
};
export const Default = Template.bind({});
Default.args = {
finalDiffableRule: {
required_fields: [
{ name: 'event.kind', type: 'keyword', ecs: true },
{ name: 'event.module', type: 'keyword', ecs: true },
],
},
};

View file

@ -0,0 +1,29 @@
/*
* 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 { EuiDescriptionList } from '@elastic/eui';
import * as ruleDetailsI18n from '../../../../translations';
import type { RequiredFieldArray } from '../../../../../../../../../common/api/detection_engine';
import { RequiredFields } from '../../../../rule_definition_section';
interface RequiredFieldsReadOnlyProps {
requiredFields: RequiredFieldArray;
}
export function RequiredFieldsReadOnly({ requiredFields }: RequiredFieldsReadOnlyProps) {
return (
<EuiDescriptionList
listItems={[
{
title: ruleDetailsI18n.REQUIRED_FIELDS_FIELD_LABEL,
description: <RequiredFields requiredFields={requiredFields} />,
},
]}
/>
);
}

View file

@ -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 type { Story } from '@storybook/react';
import { FieldReadOnly } from '../../field_readonly';
import type { DiffableAllFields } from '../../../../../../../../../common/api/detection_engine';
import { RiskScoreMappingReadOnly } from './risk_score_mapping';
export default {
component: RiskScoreMappingReadOnly,
title:
'Rule Management/Prebuilt Rules/Upgrade Flyout/ThreeWayDiff/FieldReadOnly/risk_score_mapping',
};
interface TemplateProps {
finalDiffableRule: Partial<DiffableAllFields>;
}
const Template: Story<TemplateProps> = (args) => {
return (
<FieldReadOnly
fieldName="risk_score_mapping"
finalDiffableRule={args.finalDiffableRule as DiffableAllFields}
/>
);
};
export const Default = Template.bind({});
Default.args = {
finalDiffableRule: {
risk_score_mapping: [{ field: 'event.risk_score', operator: 'equals', value: '' }],
},
};

View file

@ -0,0 +1,30 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { requiredOptional } from '@kbn/zod-helpers';
import { EuiDescriptionList } from '@elastic/eui';
import type { RiskScoreMapping } from '../../../../../../../../../common/api/detection_engine';
import * as ruleDetailsI18n from '../../../../translations';
import { RiskScoreMappingItem } from '../../../../rule_about_section';
export interface RiskScoreMappingReadProps {
riskScoreMapping: RiskScoreMapping;
}
export const RiskScoreMappingReadOnly = ({ riskScoreMapping }: RiskScoreMappingReadProps) => {
const listItems = riskScoreMapping
.filter((riskScoreMappingItem) => riskScoreMappingItem.field !== '')
.map((riskScoreMappingItem, index) => ({
title: index === 0 ? ruleDetailsI18n.RISK_SCORE_MAPPING_FIELD_LABEL : '',
description: (
<RiskScoreMappingItem riskScoreMappingItem={requiredOptional(riskScoreMappingItem)} />
),
}));
return <EuiDescriptionList listItems={listItems} />;
};

View file

@ -0,0 +1,52 @@
/*
* 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 type { Story } from '@storybook/react';
import { FieldReadOnly } from '../../field_readonly';
import type { DiffableAllFields } from '../../../../../../../../../common/api/detection_engine';
import { SeverityMappingReadOnly } from './severity_mapping';
export default {
component: SeverityMappingReadOnly,
title:
'Rule Management/Prebuilt Rules/Upgrade Flyout/ThreeWayDiff/FieldReadOnly/severity_mapping',
};
interface TemplateProps {
finalDiffableRule: Partial<DiffableAllFields>;
}
const Template: Story<TemplateProps> = (args) => {
return (
<FieldReadOnly
fieldName="severity_mapping"
finalDiffableRule={args.finalDiffableRule as DiffableAllFields}
/>
);
};
export const Default = Template.bind({});
Default.args = {
finalDiffableRule: {
severity_mapping: [
{
field: 'event.severity',
operator: 'equals',
severity: 'low',
value: 'LOW',
},
{
field: 'google_workspace.alert.metadata.severity',
operator: 'equals',
severity: 'high',
value: 'VERY HIGH',
},
],
},
};

View file

@ -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 React from 'react';
import { EuiDescriptionList } from '@elastic/eui';
import type { SeverityMapping } from '../../../../../../../../../common/api/detection_engine';
import * as ruleDetailsI18n from '../../../../translations';
import { SeverityMappingItem } from '../../../../rule_about_section';
export interface SeverityMappingReadOnlyProps {
severityMapping: SeverityMapping;
}
export const SeverityMappingReadOnly = ({ severityMapping }: SeverityMappingReadOnlyProps) => {
const listItems = severityMapping
.filter((severityMappingItem) => severityMappingItem.field !== '')
.map((severityMappingItem, index) => ({
title: index === 0 ? ruleDetailsI18n.SEVERITY_MAPPING_FIELD_LABEL : '',
description: <SeverityMappingItem severityMappingItem={severityMappingItem} />,
}));
return <EuiDescriptionList listItems={listItems} />;
};

View file

@ -0,0 +1,61 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import type { Story } from '@storybook/react';
import { FieldReadOnly } from '../../field_readonly';
import type { DiffableAllFields } from '../../../../../../../../../common/api/detection_engine';
import { ThreatReadOnly } from './threat';
export default {
component: ThreatReadOnly,
title: 'Rule Management/Prebuilt Rules/Upgrade Flyout/ThreeWayDiff/FieldReadOnly/threat',
};
interface TemplateProps {
finalDiffableRule: Partial<DiffableAllFields>;
}
const Template: Story<TemplateProps> = (args) => {
return (
<FieldReadOnly
fieldName="threat"
finalDiffableRule={args.finalDiffableRule as DiffableAllFields}
/>
);
};
export const Default = Template.bind({});
Default.args = {
finalDiffableRule: {
threat: [
{
framework: 'MITRE ATT&CK',
tactic: {
id: 'TA0006',
name: 'Credential Access',
reference: 'https://attack.mitre.org/tactics/TA0006/',
},
technique: [
{
id: 'T1003',
name: 'OS Credential Dumping',
reference: 'https://attack.mitre.org/techniques/T1003/',
subtechnique: [
{
id: 'T1003.001',
name: 'LSASS Memory',
reference: 'https://attack.mitre.org/techniques/T1003/001/',
},
],
},
],
},
],
},
};

View file

@ -0,0 +1,29 @@
/*
* 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 { EuiDescriptionList } from '@elastic/eui';
import type { Threats } from '@kbn/securitysolution-io-ts-alerting-types';
import * as ruleDetailsI18n from '../../../../translations';
import { Threat } from '../../../../rule_about_section';
export interface ThreatReadOnlyProps {
threat: Threats;
}
export const ThreatReadOnly = ({ threat }: ThreatReadOnlyProps) => {
return (
<EuiDescriptionList
listItems={[
{
title: ruleDetailsI18n.THREAT_FIELD_LABEL,
description: <Threat threat={threat} />,
},
]}
/>
);
};

View file

@ -0,0 +1,38 @@
/*
* 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 type { Story } from '@storybook/react';
import { FieldReadOnly } from '../../field_readonly';
import type { DiffableAllFields } from '../../../../../../../../../common/api/detection_engine';
import { ThreatIndexReadOnly } from './threat_index';
export default {
component: ThreatIndexReadOnly,
title: 'Rule Management/Prebuilt Rules/Upgrade Flyout/ThreeWayDiff/FieldReadOnly/threat_index',
};
interface TemplateProps {
finalDiffableRule: Partial<DiffableAllFields>;
}
const Template: Story<TemplateProps> = (args) => {
return (
<FieldReadOnly
fieldName="threat_index"
finalDiffableRule={args.finalDiffableRule as DiffableAllFields}
/>
);
};
export const Default = Template.bind({});
Default.args = {
finalDiffableRule: {
threat_index: ['logs-ti_*', 'logs-defend_*'],
},
};

View file

@ -0,0 +1,28 @@
/*
* 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 { EuiDescriptionList } from '@elastic/eui';
import * as ruleDetailsI18n from '../../../../translations';
import { ThreatIndex } from '../../../../rule_definition_section';
export interface ThreatIndexReadOnlyProps {
threatIndex: string[];
}
export const ThreatIndexReadOnly = ({ threatIndex }: ThreatIndexReadOnlyProps) => {
return (
<EuiDescriptionList
listItems={[
{
title: ruleDetailsI18n.THREAT_INDEX_FIELD_LABEL,
description: <ThreatIndex threatIndex={threatIndex} />,
},
]}
/>
);
};

View file

@ -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 type { Story } from '@storybook/react';
import { FieldReadOnly } from '../../field_readonly';
import type { DiffableAllFields } from '../../../../../../../../../common/api/detection_engine';
import { ThreatIndicatorPathReadOnly } from './threat_indicator_path';
export default {
component: ThreatIndicatorPathReadOnly,
title:
'Rule Management/Prebuilt Rules/Upgrade Flyout/ThreeWayDiff/FieldReadOnly/threat_indicator_path',
};
interface TemplateProps {
finalDiffableRule: Partial<DiffableAllFields>;
}
const Template: Story<TemplateProps> = (args) => {
return (
<FieldReadOnly
fieldName="threat_indicator_path"
finalDiffableRule={args.finalDiffableRule as DiffableAllFields}
/>
);
};
export const Default = Template.bind({});
Default.args = {
finalDiffableRule: {
threat_indicator_path: 'threat.indicator',
},
};

View file

@ -0,0 +1,34 @@
/*
* 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 { EuiDescriptionList } from '@elastic/eui';
import * as ruleDetailsI18n from '../../../../translations';
import { ThreatIndicatorPath } from '../../../../rule_about_section';
export interface ThreatIndicatorPathReadOnlyProps {
threatIndicatorPath?: string;
}
export const ThreatIndicatorPathReadOnly = ({
threatIndicatorPath,
}: ThreatIndicatorPathReadOnlyProps) => {
if (!threatIndicatorPath) {
return null;
}
return (
<EuiDescriptionList
listItems={[
{
title: ruleDetailsI18n.THREAT_INDEX_FIELD_LABEL,
description: <ThreatIndicatorPath threatIndicatorPath={threatIndicatorPath} />,
},
]}
/>
);
};

View file

@ -0,0 +1,48 @@
/*
* 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 type { Story } from '@storybook/react';
import { FieldReadOnly } from '../../field_readonly';
import type { DiffableAllFields } from '../../../../../../../../../common/api/detection_engine';
import { ThreatMappingReadOnly } from './threat_mapping';
export default {
component: ThreatMappingReadOnly,
title: 'Rule Management/Prebuilt Rules/Upgrade Flyout/ThreeWayDiff/FieldReadOnly/threat_mapping',
};
interface TemplateProps {
finalDiffableRule: Partial<DiffableAllFields>;
}
const Template: Story<TemplateProps> = (args) => {
return (
<FieldReadOnly
fieldName="threat_mapping"
finalDiffableRule={args.finalDiffableRule as DiffableAllFields}
/>
);
};
export const Default = Template.bind({});
Default.args = {
finalDiffableRule: {
threat_mapping: [
{
entries: [
{
field: 'Endpoint.capabilities',
type: 'mapping',
value: 'Target.dll.pe.description',
},
],
},
],
},
};

View file

@ -0,0 +1,29 @@
/*
* 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 { EuiDescriptionList } from '@elastic/eui';
import type { ThreatMapping } from '../../../../../../../../../common/api/detection_engine';
import { ThreatMapping as ThreatMappingComponent } from '../../../../rule_definition_section';
import * as ruleDetailsI18n from '../../../../translations';
export interface ThreatMappingReadOnlyProps {
threatMapping: ThreatMapping;
}
export const ThreatMappingReadOnly = ({ threatMapping }: ThreatMappingReadOnlyProps) => {
return (
<EuiDescriptionList
listItems={[
{
title: ruleDetailsI18n.THREAT_MAPPING_FIELD_LABEL,
description: <ThreatMappingComponent threatMapping={threatMapping} />,
},
]}
/>
);
};

View file

@ -0,0 +1,72 @@
/*
* 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 type { Story } from '@storybook/react';
import { FieldReadOnly } from '../../field_readonly';
import type { DiffableAllFields } from '../../../../../../../../../common/api/detection_engine';
import { ThreatQueryReadOnly } from './threat_query';
import {
dataSourceWithDataView,
dataSourceWithIndexPatterns,
inlineKqlQuery,
mockDataView,
} from '../../storybook/mocks';
import { ThreeWayDiffStorybookProviders } from '../../storybook/three_way_diff_storybook_providers';
export default {
component: ThreatQueryReadOnly,
title: 'Rule Management/Prebuilt Rules/Upgrade Flyout/ThreeWayDiff/FieldReadOnly/threat_query',
};
interface TemplateProps {
finalDiffableRule: Partial<DiffableAllFields>;
kibanaServicesMock?: Record<string, unknown>;
}
const Template: Story<TemplateProps> = (args) => {
return (
<ThreeWayDiffStorybookProviders kibanaServicesMock={args.kibanaServicesMock}>
<FieldReadOnly
fieldName="threat_query"
finalDiffableRule={args.finalDiffableRule as DiffableAllFields}
/>
</ThreeWayDiffStorybookProviders>
);
};
export const ThreatQueryWithIndexPatterns = Template.bind({});
ThreatQueryWithIndexPatterns.args = {
finalDiffableRule: {
threat_query: inlineKqlQuery,
data_source: dataSourceWithIndexPatterns,
},
kibanaServicesMock: {
data: {
dataViews: {
create: async () => mockDataView(),
},
},
},
};
export const ThreatQueryWithDataView = Template.bind({});
ThreatQueryWithDataView.args = {
finalDiffableRule: {
threat_query: inlineKqlQuery,
data_source: dataSourceWithDataView,
},
kibanaServicesMock: {
data: {
dataViews: {
get: async () => mockDataView(),
},
},
},
};

View file

@ -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 React from 'react';
import type {
DiffableAllFields,
InlineKqlQuery,
} from '../../../../../../../../../common/api/detection_engine';
import * as ruleDetailsI18n from '../../../../translations';
import * as descriptionStepI18n from '../../../../../../../rule_creation_ui/components/description_step/translations';
import { InlineKqlQueryReadOnly } from '../kql_query/inline_kql_query';
const i18nLabels = {
query: descriptionStepI18n.THREAT_QUERY_LABEL,
language: ruleDetailsI18n.THREAT_QUERY_LANGUAGE_LABEL,
filters: ruleDetailsI18n.THREAT_FILTERS_FIELD_LABEL,
};
export interface ThreatQueryReadOnlyProps {
threatQuery: InlineKqlQuery;
dataSource: DiffableAllFields['data_source'];
}
export const ThreatQueryReadOnly = ({ threatQuery, dataSource }: ThreatQueryReadOnlyProps) => {
return (
<InlineKqlQueryReadOnly
kqlQuery={threatQuery}
dataSource={dataSource}
i18nLabels={i18nLabels}
/>
);
};

View file

@ -0,0 +1,105 @@
/*
* 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 { FieldFormatsStartCommon } from '@kbn/field-formats-plugin/common';
import { DataView } from '@kbn/data-views-plugin/common';
import { DataSourceType, KqlQueryType } from '../../../../../../../../common/api/detection_engine';
import type {
DiffableAllFields,
SavedKqlQuery,
} from '../../../../../../../../common/api/detection_engine';
export const filters = [
{
meta: {
disabled: false,
negate: false,
alias: null,
index: 'logs-*',
key: '@timestamp',
field: '@timestamp',
value: 'exists',
type: 'exists',
},
query: {
exists: {
field: '@timestamp',
},
},
$state: {
store: 'appState',
},
},
];
export const savedQueryResponse = {
id: 'fake-saved-query-id',
attributes: {
title: 'Fake Saved Query',
description: '',
query: {
query: 'file.path: "/etc/passwd" and event.action: "modification"',
language: 'kuery',
},
filters,
},
namespaces: ['default'],
};
export const inlineKqlQuery: DiffableAllFields['kql_query'] = {
type: KqlQueryType.inline_query,
query: 'event.action: "user_login" and source.ip: "192.168.1.100"',
language: 'kuery',
filters,
};
export const savedKqlQuery: SavedKqlQuery = {
type: KqlQueryType.saved_query,
saved_query_id: 'fake-saved-query-id',
};
export const eqlQuery: DiffableAllFields['eql_query'] = {
query: 'process where process.name == "powershell.exe" and process.args : "* -EncodedCommand *"',
language: 'eql',
filters,
};
export const dataSourceWithIndexPatterns: DiffableAllFields['data_source'] = {
type: DataSourceType.index_patterns,
index_patterns: ['logs-*'],
};
export const dataSourceWithDataView: DiffableAllFields['data_source'] = {
type: DataSourceType.data_view,
data_view_id: 'logs-*',
};
type DataViewDeps = ConstructorParameters<typeof DataView>[0];
export function mockDataView(spec: Partial<DataViewDeps['spec']> = {}): DataView {
const dataView = new DataView({
spec: {
title: 'logs-*',
fields: {
'@timestamp': {
name: '@timestamp',
type: 'date',
searchable: true,
aggregatable: true,
},
},
...spec,
},
fieldFormats: {
getDefaultInstance: () => ({
toJSON: () => ({}),
}),
} as unknown as FieldFormatsStartCommon,
});
return dataView;
}

View file

@ -0,0 +1,86 @@
/*
* 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 { configureStore } from '@reduxjs/toolkit';
import { merge } from 'lodash';
import { Subject } from 'rxjs';
import { Provider as ReduxStoreProvider } from 'react-redux';
import type { CoreStart } from '@kbn/core/public';
import { createKibanaReactContext } from '@kbn/kibana-react-plugin/public';
import { ReactQueryClientProvider } from '../../../../../../../common/containers/query_client/query_client_provider';
function createKibanaServicesMock(overrides?: Partial<CoreStart>) {
const baseMock = {
data: {
dataViews: {
create: async () => {},
get: async () => {},
},
},
http: {
get: async () => {},
basePath: {
get: () => '',
},
},
notifications: {
toasts: {
addError: () => {},
addSuccess: () => {},
addWarning: () => {},
remove: () => {},
},
},
settings: {
client: {
get: (key: string, defaultOverride?: unknown) => defaultOverride,
get$: () => new Subject(),
set: () => {},
},
},
uiSettings: {},
};
return merge(baseMock, overrides);
}
function createMockStore() {
const store = configureStore({
reducer: {
app: () => ({
enableExperimental: {
prebuiltRulesCustomizationEnabled: true,
},
}),
},
});
return store;
}
interface StorybookProvidersProps {
children: React.ReactNode;
kibanaServicesMock?: Record<string, unknown>;
}
export function ThreeWayDiffStorybookProviders({
children,
kibanaServicesMock,
}: StorybookProvidersProps) {
const KibanaReactContext = createKibanaReactContext(createKibanaServicesMock(kibanaServicesMock));
const store = createMockStore();
return (
<KibanaReactContext.Provider>
<ReactQueryClientProvider>
<ReduxStoreProvider store={store}>{children}</ReduxStoreProvider>
</ReactQueryClientProvider>
</KibanaReactContext.Provider>
);
}

View file

@ -356,3 +356,38 @@ export const CUSTOMIZED_PREBUILT_RULE_LABEL = i18n.translate(
defaultMessage: 'Customized Elastic rule',
}
);
export const QUERY_LANGUAGE_LABEL = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleDetails.queryLanguageLabel',
{
defaultMessage: 'Custom query language',
}
);
export const THREAT_QUERY_LANGUAGE_LABEL = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleDetails.threatQueryLanguageLabel',
{
defaultMessage: 'Indicator index query language',
}
);
export const SAVED_QUERY_LANGUAGE_LABEL = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleDetails.savedQueryLanguageLabel',
{
defaultMessage: 'Saved query language',
}
);
export const KUERY_LANGUAGE_LABEL = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleDetails.kqlLanguageLabel',
{
defaultMessage: 'KQL',
}
);
export const LUCENE_LANGUAGE_LABEL = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleDetails.luceneLanguageLabel',
{
defaultMessage: 'Lucene',
}
);

View file

@ -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 { useKibana } from '../../../../common/lib/kibana/kibana_react';
import { DEFAULT_INDEX_KEY, DEFAULT_INDEX_PATTERN } from '../../../../../common/constants';
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
/**
* Gets the default index pattern for cases when rule has neither index patterns or data view.
* First checks the config value. If it's not present falls back to the hardcoded default value.
*/
export function useDefaultIndexPattern() {
const { services } = useKibana();
const isPrebuiltRulesCustomizationEnabled = useIsExperimentalFeatureEnabled(
'prebuiltRulesCustomizationEnabled'
);
if (isPrebuiltRulesCustomizationEnabled) {
return services.settings.client.get(DEFAULT_INDEX_KEY, DEFAULT_INDEX_PATTERN);
}
return [];
}