Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Security Solution] Add a tour showing new rules search capabilities #128849

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion x-pack/plugins/security_solution/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -443,7 +443,7 @@ export const RULES_TABLE_PAGE_SIZE_OPTIONS = [5, 10, 20, 50, RULES_TABLE_MAX_PAG
* we will need to update this constant with the corresponding version.
*/
export const RULES_MANAGEMENT_FEATURE_TOUR_STORAGE_KEY =
'securitySolution.rulesManagementPage.newFeaturesTour.v8.1';
'securitySolution.rulesManagementPage.newFeaturesTour.v8.2';

export const RULE_DETAILS_EXECUTION_LOG_TABLE_SHOW_METRIC_COLUMNS_STORAGE_KEY =
'securitySolution.ruleDetails.ruleExecutionLog.showMetrics.v8.2';
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ describe('Detections Rules API', () => {
method: 'GET',
query: {
filter:
'(alert.attributes.name: "hello world" OR alert.attributes.params.index: "hello world" OR alert.attributes.params.threat.tactic.id: "hello world" OR alert.attributes.params.threat.tactic.name: "hello world" OR alert.attributes.params.threat.technique.id: "hello world" OR alert.attributes.params.threat.technique.name: "hello world")',
'(alert.attributes.name: "hello world" OR alert.attributes.params.index: "hello world" OR alert.attributes.params.threat.tactic.id: "hello world" OR alert.attributes.params.threat.tactic.name: "hello world" OR alert.attributes.params.threat.technique.id: "hello world" OR alert.attributes.params.threat.technique.name: "hello world" OR alert.attributes.params.threat.technique.subtechnique.id: "hello world" OR alert.attributes.params.threat.technique.subtechnique.name: "hello world")',
page: 1,
per_page: 20,
sort_field: 'enabled',
Expand Down Expand Up @@ -172,7 +172,7 @@ describe('Detections Rules API', () => {
method: 'GET',
query: {
filter:
'(alert.attributes.name: "\\" OR (foo:bar)" OR alert.attributes.params.index: "\\" OR (foo:bar)" OR alert.attributes.params.threat.tactic.id: "\\" OR (foo:bar)" OR alert.attributes.params.threat.tactic.name: "\\" OR (foo:bar)" OR alert.attributes.params.threat.technique.id: "\\" OR (foo:bar)" OR alert.attributes.params.threat.technique.name: "\\" OR (foo:bar)")',
'(alert.attributes.name: "\\" OR (foo:bar)" OR alert.attributes.params.index: "\\" OR (foo:bar)" OR alert.attributes.params.threat.tactic.id: "\\" OR (foo:bar)" OR alert.attributes.params.threat.tactic.name: "\\" OR (foo:bar)" OR alert.attributes.params.threat.technique.id: "\\" OR (foo:bar)" OR alert.attributes.params.threat.technique.name: "\\" OR (foo:bar)" OR alert.attributes.params.threat.technique.subtechnique.id: "\\" OR (foo:bar)" OR alert.attributes.params.threat.technique.subtechnique.name: "\\" OR (foo:bar)")',
page: 1,
per_page: 20,
sort_field: 'enabled',
Expand Down Expand Up @@ -383,7 +383,7 @@ describe('Detections Rules API', () => {
method: 'GET',
query: {
filter:
'alert.attributes.tags: "__internal_immutable:false" AND alert.attributes.tags: "__internal_immutable:true" AND alert.attributes.tags:("hello" AND "world") AND (alert.attributes.name: "ruleName" OR alert.attributes.params.index: "ruleName" OR alert.attributes.params.threat.tactic.id: "ruleName" OR alert.attributes.params.threat.tactic.name: "ruleName" OR alert.attributes.params.threat.technique.id: "ruleName" OR alert.attributes.params.threat.technique.name: "ruleName")',
'alert.attributes.tags: "__internal_immutable:false" AND alert.attributes.tags: "__internal_immutable:true" AND alert.attributes.tags:("hello" AND "world") AND (alert.attributes.name: "ruleName" OR alert.attributes.params.index: "ruleName" OR alert.attributes.params.threat.tactic.id: "ruleName" OR alert.attributes.params.threat.tactic.name: "ruleName" OR alert.attributes.params.threat.technique.id: "ruleName" OR alert.attributes.params.threat.technique.name: "ruleName" OR alert.attributes.params.threat.technique.subtechnique.id: "ruleName" OR alert.attributes.params.threat.technique.subtechnique.name: "ruleName")',
page: 1,
per_page: 20,
sort_field: 'enabled',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,15 @@ describe('convertRulesFilterToKQL', () => {
const kql = convertRulesFilterToKQL({ ...filterOptions, filter: 'foo' });

expect(kql).toBe(
'(alert.attributes.name: "foo" OR alert.attributes.params.index: "foo" OR alert.attributes.params.threat.tactic.id: "foo" OR alert.attributes.params.threat.tactic.name: "foo" OR alert.attributes.params.threat.technique.id: "foo" OR alert.attributes.params.threat.technique.name: "foo")'
'(alert.attributes.name: "foo" OR alert.attributes.params.index: "foo" OR alert.attributes.params.threat.tactic.id: "foo" OR alert.attributes.params.threat.tactic.name: "foo" OR alert.attributes.params.threat.technique.id: "foo" OR alert.attributes.params.threat.technique.name: "foo" OR alert.attributes.params.threat.technique.subtechnique.id: "foo" OR alert.attributes.params.threat.technique.subtechnique.name: "foo")'
);
});

it('escapes "filter" value properly', () => {
const kql = convertRulesFilterToKQL({ ...filterOptions, filter: '" OR (foo: bar)' });

expect(kql).toBe(
'(alert.attributes.name: "\\" OR (foo: bar)" OR alert.attributes.params.index: "\\" OR (foo: bar)" OR alert.attributes.params.threat.tactic.id: "\\" OR (foo: bar)" OR alert.attributes.params.threat.tactic.name: "\\" OR (foo: bar)" OR alert.attributes.params.threat.technique.id: "\\" OR (foo: bar)" OR alert.attributes.params.threat.technique.name: "\\" OR (foo: bar)")'
'(alert.attributes.name: "\\" OR (foo: bar)" OR alert.attributes.params.index: "\\" OR (foo: bar)" OR alert.attributes.params.threat.tactic.id: "\\" OR (foo: bar)" OR alert.attributes.params.threat.tactic.name: "\\" OR (foo: bar)" OR alert.attributes.params.threat.technique.id: "\\" OR (foo: bar)" OR alert.attributes.params.threat.technique.name: "\\" OR (foo: bar)" OR alert.attributes.params.threat.technique.subtechnique.id: "\\" OR (foo: bar)" OR alert.attributes.params.threat.technique.subtechnique.name: "\\" OR (foo: bar)")'
);
});

Expand Down Expand Up @@ -66,7 +66,7 @@ describe('convertRulesFilterToKQL', () => {
});

expect(kql).toBe(
`alert.attributes.tags: "${INTERNAL_IMMUTABLE_KEY}:true" AND alert.attributes.tags:(\"tag1\" AND \"tag2\") AND (alert.attributes.name: \"foo\" OR alert.attributes.params.index: \"foo\" OR alert.attributes.params.threat.tactic.id: \"foo\" OR alert.attributes.params.threat.tactic.name: \"foo\" OR alert.attributes.params.threat.technique.id: \"foo\" OR alert.attributes.params.threat.technique.name: \"foo\")`
`alert.attributes.tags: "${INTERNAL_IMMUTABLE_KEY}:true" AND alert.attributes.tags:("tag1" AND "tag2") AND (alert.attributes.name: "foo" OR alert.attributes.params.index: "foo" OR alert.attributes.params.threat.tactic.id: "foo" OR alert.attributes.params.threat.tactic.name: "foo" OR alert.attributes.params.threat.technique.id: "foo" OR alert.attributes.params.threat.technique.name: "foo" OR alert.attributes.params.threat.technique.subtechnique.id: "foo" OR alert.attributes.params.threat.technique.subtechnique.name: "foo")`
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ const SEARCHABLE_RULE_PARAMS = [
'alert.attributes.params.threat.tactic.name',
'alert.attributes.params.threat.technique.id',
'alert.attributes.params.threat.technique.name',
'alert.attributes.params.threat.technique.subtechnique.id',
'alert.attributes.params.threat.technique.subtechnique.name',
];

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,23 +43,13 @@ const tourConfig: EuiTourState = {
const stepsConfig: EuiStatelessTourStep[] = [
{
step: 1,
title: 'A new feature',
content: <p>{'This feature allows for...'}</p>,
stepsTotal: 2,
title: i18n.SEARCH_CAPABILITIES_TITLE,
content: <p>{i18n.SEARCH_CAPABILITIES_DESCRIPTION}</p>,
stepsTotal: 1,
children: <></>,
onFinish: noop,
maxWidth: TOUR_POPOVER_WIDTH,
},
{
step: 2,
title: 'Another feature',
content: <p>{'This another feature allows for...'}</p>,
stepsTotal: 2,
children: <></>,
onFinish: noop,
anchorPosition: 'rightUp',
maxWidth: TOUR_POPOVER_WIDTH,
},
];

const RulesFeatureTourContext = createContext<RulesFeatureTourContextType | null>(null);
Expand All @@ -82,39 +72,43 @@ export const RulesFeatureTourContextProvider: FC = ({ children }) => {

const [tourSteps, tourActions, tourState] = useEuiTour(stepsConfig, restoredState);

const enhancedSteps = useMemo<EuiTourStepProps[]>(() => {
return tourSteps.map((item, index, array) => {
return {
const enhancedSteps = useMemo<EuiTourStepProps[]>(
() =>
tourSteps.map((item, index) => ({
...item,
content: (
<>
{item.content}
<EuiSpacer size="s" />
<EuiFlexGroup responsive={false} gutterSize="s" alignItems="center">
<EuiFlexItem grow={false}>
<EuiButtonIcon
iconType="arrowLeft"
aria-label="Go to previous step"
display="empty"
disabled={index === 0}
onClick={tourActions.decrementStep}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonIcon
iconType="arrowRight"
aria-label="Go to next step"
display="base"
disabled={index === array.length - 1}
onClick={tourActions.incrementStep}
/>
</EuiFlexItem>
</EuiFlexGroup>
{tourSteps.length > 1 && (
<>
<EuiSpacer size="s" />
<EuiFlexGroup responsive={false} gutterSize="s" alignItems="center">
<EuiFlexItem grow={false}>
<EuiButtonIcon
iconType="arrowLeft"
aria-label="Go to previous step"
display="empty"
disabled={index === 0}
onClick={tourActions.decrementStep}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonIcon
iconType="arrowRight"
aria-label="Go to next step"
display="base"
disabled={index === tourSteps.length - 1}
onClick={tourActions.incrementStep}
/>
</EuiFlexItem>
</EuiFlexGroup>
</>
)}
</>
),
};
});
}, [tourSteps, tourActions]);
})),
[tourSteps, tourActions]
);

const providerValue = useMemo<RulesFeatureTourContextType>(
() => ({ steps: enhancedSteps, actions: tourActions }),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,18 @@ export const TOUR_TITLE = i18n.translate(
defaultMessage: "What's new",
}
);

export const SEARCH_CAPABILITIES_TITLE = i18n.translate(
'xpack.securitySolution.detectionEngine.rules.allRules.featureTour.searchCapabilitiesTitle',
{
defaultMessage: 'Enhanced search capabilities',
}
);

export const SEARCH_CAPABILITIES_DESCRIPTION = i18n.translate(
'xpack.securitySolution.detectionEngine.rules.allRules.featureTour.searchCapabilitiesDescription',
{
defaultMessage:
'It is now possible to search rules by index patterns, like "filebeat-*", or by MITRE ATT&CK™ tactics or techniques, like "Defense Evasion" or "TA0005".',
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { useKibana } from '../../../../../common/lib/kibana';
import { TestProviders } from '../../../../../common/mock';
import '../../../../../common/mock/formatted_relative';
import '../../../../../common/mock/match_media';
import { RulesFeatureTourContextProvider } from './feature_tour/rules_feature_tour_context';
import { AllRules } from './index';

jest.mock('../../../../../common/components/link_to');
Expand Down Expand Up @@ -67,7 +68,8 @@ describe('AllRules', () => {
rulesNotInstalled={0}
rulesNotUpdated={0}
/>
</TestProviders>
</TestProviders>,
{ wrappingComponent: RulesFeatureTourContextProvider }
);

await waitFor(() => {
Expand All @@ -90,7 +92,8 @@ describe('AllRules', () => {
rulesNotInstalled={0}
rulesNotUpdated={0}
/>
</TestProviders>
</TestProviders>,
{ wrappingComponent: RulesFeatureTourContextProvider }
);

await waitFor(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,16 @@ import { mount } from 'enzyme';

import { RulesTableFilters } from './rules_table_filters';
import { TestProviders } from '../../../../../../common/mock';
import { RulesFeatureTourContextProvider } from '../feature_tour/rules_feature_tour_context';

jest.mock('../rules_table/rules_table_context');

describe('RulesTableFilters', () => {
it('renders no numbers next to rule type button filter if none exist', async () => {
const wrapper = mount(
<RulesTableFilters rulesCustomInstalled={null} rulesInstalled={null} allTags={[]} />,
<RulesFeatureTourContextProvider>
<RulesTableFilters rulesCustomInstalled={null} rulesInstalled={null} allTags={[]} />
</RulesFeatureTourContextProvider>,
{ wrappingComponent: TestProviders }
);

Expand All @@ -30,7 +33,9 @@ describe('RulesTableFilters', () => {

it('renders number of custom and prepackaged rules', async () => {
const wrapper = mount(
<RulesTableFilters rulesCustomInstalled={10} rulesInstalled={9} allTags={[]} />,
<RulesFeatureTourContextProvider>
<RulesTableFilters rulesCustomInstalled={10} rulesInstalled={9} allTags={[]} />
</RulesFeatureTourContextProvider>,
{ wrappingComponent: TestProviders }
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,28 @@ import {
EuiFilterGroup,
EuiFlexGroup,
EuiFlexItem,
EuiTourStep,
} from '@elastic/eui';
import { isEqual } from 'lodash/fp';
import React, { useCallback } from 'react';
import styled from 'styled-components';
import * as i18n from '../../translations';
import { useRulesFeatureTourContext } from '../feature_tour/rules_feature_tour_context';
import { useRulesTableContext } from '../rules_table/rules_table_context';
import { TagsFilterPopover } from './tags_filter_popover';

const FilterWrapper = styled(EuiFlexGroup)`
margin-bottom: ${({ theme }) => theme.eui.euiSizeXS};
`;

const SearchBarWrapper = styled(EuiFlexItem)`
& .euiPopover__anchor {
// This is needed to "cancel" styles passed down from EuiTourStep that
// interfere with EuiFieldSearch and don't allow it to take the full width
display: block;
}
`;

interface RulesTableFiltersProps {
rulesCustomInstalled: number | null;
rulesInstalled: number | null;
Expand All @@ -45,6 +55,8 @@ const RulesTableFiltersComponent = ({

const { showCustomRules, showElasticRules, tags: selectedTags } = filterOptions;

const { steps } = useRulesFeatureTourContext();

const handleOnSearch = useCallback(
(filterString) => setFilterOptions({ filter: filterString.trim() }),
[setFilterOptions]
Expand All @@ -69,15 +81,17 @@ const RulesTableFiltersComponent = ({

return (
<FilterWrapper gutterSize="m" justifyContent="flexEnd">
<EuiFlexItem grow>
<EuiFieldSearch
aria-label={i18n.SEARCH_RULES}
fullWidth
incremental={false}
placeholder={i18n.SEARCH_PLACEHOLDER}
onSearch={handleOnSearch}
/>
</EuiFlexItem>
<SearchBarWrapper grow>
<EuiTourStep {...steps[0]} anchorPosition="downLeft">
<EuiFieldSearch
aria-label={i18n.SEARCH_RULES}
fullWidth
incremental={false}
placeholder={i18n.SEARCH_PLACEHOLDER}
onSearch={handleOnSearch}
/>
</EuiTourStep>
</SearchBarWrapper>
<EuiFlexItem grow={false}>
<EuiFilterGroup>
<TagsFilterPopover
Expand Down
Loading