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] Added guided onboarding for the rules area #144016

Merged
merged 3 commits into from
Oct 31, 2022
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ import { useInvalidateFetchPrebuiltRulesStatusQuery } from './use_fetch_prebuilt
import { useInvalidateFindRulesQuery, useUpdateRulesCache } from './use_find_rules_query';
import { useInvalidateFetchTagsQuery } from './use_fetch_tags_query';
import { useInvalidateFetchRuleByIdQuery } from './use_fetch_rule_by_id_query';
import { DETECTION_ENGINE_RULES_BULK_ACTION } from '../../../../../common/constants';

export const BULK_ACTION_MUTATION_KEY = ['POST', DETECTION_ENGINE_RULES_BULK_ACTION];

export const useBulkActionMutation = (
options?: UseMutationOptions<BulkActionResponse, Error, BulkActionProps>
Expand All @@ -27,6 +30,7 @@ export const useBulkActionMutation = (
(action: BulkActionProps) => performBulkAction(action),
{
...options,
mutationKey: BULK_ACTION_MUTATION_KEY,
onSuccess: (...args) => {
const [res, { action }] = args;
switch (action) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,20 @@
*/
import type { UseMutationOptions } from '@tanstack/react-query';
import { useMutation } from '@tanstack/react-query';
import { DETECTION_ENGINE_RULES_BULK_ACTION } from '../../../../../common/constants';
import type { BulkExportProps, BulkExportResponse } from '../api';
import { bulkExportRules } from '../api';

export const BULK_ACTION_MUTATION_KEY = ['POST', DETECTION_ENGINE_RULES_BULK_ACTION];

export const useBulkExportMutation = (
options?: UseMutationOptions<BulkExportResponse, Error, BulkExportProps>
) => {
return useMutation<BulkExportResponse, Error, BulkExportProps>(
(action: BulkExportProps) => bulkExportRules(action),
options
{
...options,
mutationKey: BULK_ACTION_MUTATION_KEY,
}
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import { createPrepackagedRules } from '../api';
import { useInvalidateFetchPrebuiltRulesStatusQuery } from './use_fetch_prebuilt_rules_status_query';
import { useInvalidateFindRulesQuery } from './use_find_rules_query';
import { useInvalidateFetchTagsQuery } from './use_fetch_tags_query';
import { PREBUILT_RULES_URL } from '../../../../../common/detection_engine/prebuilt_rules/api/urls';

export const CREATE_PREBUILT_RULES_MUTATION_KEY = ['PUT', PREBUILT_RULES_URL];

export const useCreatePrebuiltRulesMutation = (
options?: UseMutationOptions<CreatePrepackagedRulesResponse>
Expand All @@ -21,6 +24,7 @@ export const useCreatePrebuiltRulesMutation = (

return useMutation(() => createPrepackagedRules(), {
...options,
mutationKey: CREATE_PREBUILT_RULES_MUTATION_KEY,
onSuccess: (...args) => {
// Always invalidate all rules and the prepackaged rules status cache as
// the number of rules might change after the installation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/
import type { UseMutationOptions } from '@tanstack/react-query';
import { useMutation } from '@tanstack/react-query';
import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants';
import type {
RuleCreateProps,
RuleResponse,
Expand All @@ -16,6 +17,8 @@ import { useInvalidateFetchPrebuiltRulesStatusQuery } from './use_fetch_prebuilt
import { useInvalidateFetchTagsQuery } from './use_fetch_tags_query';
import { useInvalidateFindRulesQuery } from './use_find_rules_query';

export const CREATE_RULE_MUTATION_KEY = ['POST', DETECTION_ENGINE_RULES_URL];

export const useCreateRuleMutation = (
options?: UseMutationOptions<RuleResponse, Error, RuleCreateProps>
) => {
Expand All @@ -27,6 +30,7 @@ export const useCreateRuleMutation = (
(rule: RuleCreateProps) => createRule({ rule: transformOutput(rule) }),
{
...options,
mutationKey: CREATE_RULE_MUTATION_KEY,
onSuccess: (...args) => {
invalidateFetchPrePackagedRulesStatusQuery();
invalidateFindRulesQuery();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,15 @@ import { useQuery, useQueryClient } from '@tanstack/react-query';
import { getPrePackagedRulesStatus } from '../api';
import { DEFAULT_QUERY_OPTIONS } from './constants';
import type { PrePackagedRulesStatusResponse } from '../../logic';
import { PREBUILT_RULES_STATUS_URL } from '../../../../../common/detection_engine/prebuilt_rules/api/urls';

export const PREBUILT_RULES_STATUS_QUERY_KEY = 'prePackagedRulesStatus';
export const PREBUILT_RULES_STATUS_QUERY_KEY = ['GET', PREBUILT_RULES_STATUS_URL];

export const useFetchPrebuiltRulesStatusQuery = (
options: UseQueryOptions<PrePackagedRulesStatusResponse>
options?: UseQueryOptions<PrePackagedRulesStatusResponse>
) => {
return useQuery<PrePackagedRulesStatusResponse>(
[PREBUILT_RULES_STATUS_QUERY_KEY],
PREBUILT_RULES_STATUS_QUERY_KEY,
async ({ signal }) => {
const response = await getPrePackagedRulesStatus({ signal });
return response;
Expand All @@ -40,7 +41,7 @@ export const useInvalidateFetchPrebuiltRulesStatusQuery = () => {
const queryClient = useQueryClient();

return useCallback(() => {
queryClient.invalidateQueries([PREBUILT_RULES_STATUS_QUERY_KEY], {
queryClient.invalidateQueries(PREBUILT_RULES_STATUS_QUERY_KEY, {
refetchType: 'active',
});
}, [queryClient]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@
import type { UseQueryOptions } from '@tanstack/react-query';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { useCallback } from 'react';
import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants';
import { transformInput } from '../../../../detections/containers/detection_engine/rules/transforms';
import type { Rule } from '../../logic';
import { fetchRuleById } from '../api';
import { DEFAULT_QUERY_OPTIONS } from './constants';

const FIND_ONE_RULE_QUERY_KEY = 'findOneRule';
const FIND_ONE_RULE_QUERY_KEY = ['GET', DETECTION_ENGINE_RULES_URL];

/**
* A wrapper around useQuery provides default values to the underlying query,
Expand All @@ -23,9 +24,9 @@ const FIND_ONE_RULE_QUERY_KEY = 'findOneRule';
* @param options - react-query options
* @returns useQuery result
*/
export const useFetchRuleByIdQuery = (id: string, options: UseQueryOptions<Rule>) => {
export const useFetchRuleByIdQuery = (id: string, options?: UseQueryOptions<Rule>) => {
return useQuery<Rule>(
[FIND_ONE_RULE_QUERY_KEY, id],
[...FIND_ONE_RULE_QUERY_KEY, id],
async ({ signal }) => {
const response = await fetchRuleById({ signal, id });

Expand All @@ -49,7 +50,7 @@ export const useInvalidateFetchRuleByIdQuery = () => {
const queryClient = useQueryClient();

return useCallback(() => {
queryClient.invalidateQueries([FIND_ONE_RULE_QUERY_KEY], {
queryClient.invalidateQueries(FIND_ONE_RULE_QUERY_KEY, {
refetchType: 'active',
});
}, [queryClient]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,20 @@
import type { UseQueryOptions } from '@tanstack/react-query';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { useCallback } from 'react';
import { DETECTION_ENGINE_TAGS_URL } from '../../../../../common/constants';
import type { FetchTagsResponse } from '../api';
import { fetchTags } from '../api';
import { DEFAULT_QUERY_OPTIONS } from './constants';

// TODO: https://github.com/elastic/kibana/pull/142950 Let's use more detailed cache keys, e.g. ['GET', DETECTION_ENGINE_TAGS_URL]
const TAGS_QUERY_KEY = 'tags';
const TAGS_QUERY_KEY = ['GET', DETECTION_ENGINE_TAGS_URL];

/**
* Hook for using the list of Tags from the Detection Engine API
*
*/
export const useFetchTagsQuery = (options?: UseQueryOptions<FetchTagsResponse>) => {
return useQuery<FetchTagsResponse>(
[TAGS_QUERY_KEY],
TAGS_QUERY_KEY,
async ({ signal }) => {
return fetchTags({ signal });
},
Expand All @@ -36,7 +36,7 @@ export const useInvalidateFetchTagsQuery = () => {
const queryClient = useQueryClient();

return useCallback(() => {
queryClient.invalidateQueries([TAGS_QUERY_KEY], {
queryClient.invalidateQueries(TAGS_QUERY_KEY, {
refetchType: 'active',
});
}, [queryClient]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import type { UseQueryOptions } from '@tanstack/react-query';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { useCallback } from 'react';
import { DETECTION_ENGINE_RULES_URL_FIND } from '../../../../../common/constants';
import type { FilterOptions, PaginationOptions, Rule, SortingOptions } from '../../logic';
import { fetchRules } from '../api';
import { DEFAULT_QUERY_OPTIONS } from './constants';
Expand All @@ -18,7 +19,7 @@ export interface FindRulesQueryArgs {
pagination?: Pick<PaginationOptions, 'page' | 'perPage'>;
}

const FIND_RULES_QUERY_KEY = 'findRules';
const FIND_RULES_QUERY_KEY = ['GET', DETECTION_ENGINE_RULES_URL_FIND];

export interface RulesQueryResponse {
rules: Rule[];
Expand All @@ -37,15 +38,15 @@ export interface RulesQueryResponse {
*/
export const useFindRulesQuery = (
queryArgs: FindRulesQueryArgs,
queryOptions: UseQueryOptions<
queryOptions?: UseQueryOptions<
RulesQueryResponse,
Error,
RulesQueryResponse,
[...string[], FindRulesQueryArgs]
>
) => {
return useQuery(
[FIND_RULES_QUERY_KEY, queryArgs],
[...FIND_RULES_QUERY_KEY, queryArgs],
async ({ signal }) => {
const response = await fetchRules({ signal, ...queryArgs });

Expand Down Expand Up @@ -73,7 +74,7 @@ export const useInvalidateFindRulesQuery = () => {
* Invalidate all queries that start with FIND_RULES_QUERY_KEY. This
* includes the in-memory query cache and paged query cache.
*/
queryClient.invalidateQueries([FIND_RULES_QUERY_KEY], {
queryClient.invalidateQueries(FIND_RULES_QUERY_KEY, {
refetchType: 'active',
});
}, [queryClient]);
Expand All @@ -98,7 +99,7 @@ export const useUpdateRulesCache = () => {
return useCallback(
(newRules: Rule[]) => {
queryClient.setQueriesData<ReturnType<typeof useFindRulesQuery>['data']>(
[FIND_RULES_QUERY_KEY],
FIND_RULES_QUERY_KEY,
(currentData) =>
currentData
? {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ import { updateRule } from '../api';
import { useInvalidateFindRulesQuery } from './use_find_rules_query';
import { useInvalidateFetchTagsQuery } from './use_fetch_tags_query';
import { useInvalidateFetchRuleByIdQuery } from './use_fetch_rule_by_id_query';
import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants';

export const UPDATE_RULE_MUTATION_KEY = ['PUT', DETECTION_ENGINE_RULES_URL];

export const useUpdateRuleMutation = (
options?: UseMutationOptions<RuleResponse, Error, RuleUpdateProps>
Expand All @@ -27,6 +30,7 @@ export const useUpdateRuleMutation = (
(rule: RuleUpdateProps) => updateRule({ rule: transformOutput(rule) }),
{
...options,
mutationKey: UPDATE_RULE_MUTATION_KEY,
onSuccess: (...args) => {
invalidateFindRulesQuery();
invalidateFetchRuleByIdQuery();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/*
* 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 { EuiTourActions, EuiTourStepProps } from '@elastic/eui';
import { EuiTourStep } from '@elastic/eui';
import { noop } from 'lodash';
import React, { useEffect, useMemo } from 'react';
import useObservable from 'react-use/lib/useObservable';
import { of } from 'rxjs';
import { useKibana } from '../../../../common/lib/kibana';
import { useFindRulesQuery } from '../../../rule_management/api/hooks/use_find_rules_query';
import * as i18n from './translations';
import { useIsElementMounted } from './use_is_element_mounted';

export const INSTALL_PREBUILT_RULES_ANCHOR = 'install-prebuilt-rules-anchor';
export const SEARCH_FIRST_RULE_ANCHOR = 'search-first-rule-anchor';

export interface RulesFeatureTourContextType {
steps: EuiTourStepProps[];
actions: EuiTourActions;
}

const GUIDED_ONBOARDING_RULES_FILTER = {
filter: '',
showCustomRules: false,
showElasticRules: true,
tags: ['Guided Onboarding'],
};

export enum GuidedOnboardingRulesStatus {
'inactive' = 'inactive',
'installRules' = 'installRules',
'activateRules' = 'activateRules',
'completed' = 'completed',
}

export const RulesManagementTour = () => {
const { guidedOnboardingApi } = useKibana().services.guidedOnboarding;

const isRulesStepActive = useObservable(
guidedOnboardingApi?.isGuideStepActive$('security', 'rules') ?? of(false),
false
);

const { data: onboardingRules } = useFindRulesQuery(
{ filterOptions: GUIDED_ONBOARDING_RULES_FILTER },
{ enabled: isRulesStepActive }
);

const tourStatus = useMemo(() => {
if (!isRulesStepActive || !onboardingRules) {
return GuidedOnboardingRulesStatus.inactive;
}

if (onboardingRules.total === 0) {
// Onboarding rules are not installed - show the install/update rules step
return GuidedOnboardingRulesStatus.installRules;
}

if (!onboardingRules.rules.some((rule) => rule.enabled)) {
// None of the onboarding rules is active - show the activate step
return GuidedOnboardingRulesStatus.activateRules;
}

// Rules are installed and enabled - the tour is completed
return GuidedOnboardingRulesStatus.completed;
}, [isRulesStepActive, onboardingRules]);

// Synchronize the current "internal" tour step with the global one
useEffect(() => {
if (isRulesStepActive && tourStatus === GuidedOnboardingRulesStatus.completed) {
guidedOnboardingApi?.completeGuideStep('security', 'rules');
}
}, [guidedOnboardingApi, isRulesStepActive, tourStatus]);

/**
* Wait until the tour target elements are visible on the page and mount
* EuiTourStep components only after that. Otherwise, the tours would never
* show up on the page.
*/
const isInstallRulesAnchorMounted = useIsElementMounted(INSTALL_PREBUILT_RULES_ANCHOR);
const isSearchFirstRuleAnchorMounted = useIsElementMounted(SEARCH_FIRST_RULE_ANCHOR);

return (
<>
{isInstallRulesAnchorMounted && (
<EuiTourStep
title={i18n.INSTALL_PREBUILT_RULES_TITLE}
content={i18n.INSTALL_PREBUILT_RULES_CONTENT}
onFinish={noop}
step={1}
stepsTotal={2}
isOpen={tourStatus === GuidedOnboardingRulesStatus.installRules}
anchor={`#${INSTALL_PREBUILT_RULES_ANCHOR}`}
anchorPosition="downCenter"
footerAction={<div />} // Replace "Skip tour" with an empty element
/>
)}
{isSearchFirstRuleAnchorMounted && (
<EuiTourStep
title={i18n.SEARCH_FIRST_RULE_TITLE}
content={i18n.SEARCH_FIRST_RULE_CONTENT}
onFinish={noop}
step={2}
stepsTotal={2}
isOpen={tourStatus === GuidedOnboardingRulesStatus.activateRules}
anchor={`#${SEARCH_FIRST_RULE_ANCHOR}`}
anchorPosition="upCenter"
footerAction={<div />} // Replace "Skip tour" with an empty element
/>
)}
</>
);
};
Loading