Skip to content

Commit 4afde99

Browse files
authored
[Automatic Migration] Add ability to skip Reference Set step in QRadar upload workflow (elastic#259959)
## Summary - Handles elastic/security-team#16315 Adds the ability for users to skip the Reference Set upload step (Step 2) in the QRadar rule migration workflow, allowing them to proceed directly to the Enhancements step (Step 3 - MITRE Mappings) without uploading all reference sets. ### Demo https://github.com/user-attachments/assets/a4ce3e18-1020-46f5-9e6f-426ad8f4616b ## Test plan - [ ] Upload QRadar rules with missing reference sets - [ ] Verify "Skip" button appears in the Reference Set step header when step is active - [ ] Click "Skip" and verify Enhancements step becomes active - [ ] Verify uploading all reference sets still advances to Enhancements step automatically - [ ] Re-open the flyout and verify reference set step can still be completed - [ ] Verify Splunk workflow is unaffected
1 parent ef5890a commit 4afde99

9 files changed

Lines changed: 190 additions & 17 deletions

File tree

x-pack/solutions/security/plugins/security_solution/public/siem_migrations/common/components/migration_steps/lookups/lookups_file_upload/index.test.tsx

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
*/
77

88
import React from 'react';
9-
import { render } from '@testing-library/react';
9+
import { fireEvent, render } from '@testing-library/react';
1010
import { LookupsFileUpload } from '.';
1111
import { MigrationSource } from '../../../../types';
1212

@@ -22,4 +22,22 @@ describe('LookupsFileUpload', () => {
2222
const { getByTestId } = render(<LookupsFileUpload {...props} />);
2323
expect(getByTestId('uploadFileButton')).toBeInTheDocument();
2424
});
25+
26+
it('does not render skip button when onSkip is not provided', () => {
27+
const { queryByTestId } = render(<LookupsFileUpload {...props} />);
28+
expect(queryByTestId('lookupsUploadSkipButton')).not.toBeInTheDocument();
29+
});
30+
31+
it('renders skip button when onSkip is provided', () => {
32+
const { getByTestId } = render(<LookupsFileUpload {...props} onSkip={jest.fn()} />);
33+
expect(getByTestId('lookupsUploadSkipButton')).toBeInTheDocument();
34+
expect(getByTestId('lookupsUploadSkipButton')).toHaveTextContent('Skip');
35+
});
36+
37+
it('calls onSkip when skip button is clicked', () => {
38+
const onSkip = jest.fn();
39+
const { getByTestId } = render(<LookupsFileUpload {...props} onSkip={onSkip} />);
40+
fireEvent.click(getByTestId('lookupsUploadSkipButton'));
41+
expect(onSkip).toHaveBeenCalledTimes(1);
42+
});
2543
});

x-pack/solutions/security/plugins/security_solution/public/siem_migrations/common/components/migration_steps/lookups/lookups_file_upload/index.tsx

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,14 @@
66
*/
77

88
import React, { useCallback, useMemo, useRef, useState } from 'react';
9-
import { EuiFilePicker, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiText } from '@elastic/eui';
9+
import {
10+
EuiButtonEmpty,
11+
EuiFilePicker,
12+
EuiFlexGroup,
13+
EuiFlexItem,
14+
EuiFormRow,
15+
EuiText,
16+
} from '@elastic/eui';
1017
import type {
1118
EuiFilePickerClass,
1219
EuiFilePickerProps,
@@ -23,6 +30,7 @@ export interface LookupsFileUploadProps {
2330
apiError?: string;
2431
isLoading?: boolean;
2532
migrationSource: MigrationSource;
33+
onSkip?: () => void;
2634
}
2735

2836
const CONFIGS: Record<MigrationSource, { prompt: string; label: string }> = {
@@ -37,7 +45,7 @@ const CONFIGS: Record<MigrationSource, { prompt: string; label: string }> = {
3745
};
3846

3947
export const LookupsFileUpload = React.memo<LookupsFileUploadProps>(
40-
({ createResources, apiError, isLoading, migrationSource }) => {
48+
({ createResources, apiError, isLoading, migrationSource, onSkip }) => {
4149
const [lookupResources, setLookupResources] = useState<SiemMigrationResourceData[]>([]);
4250
const filePickerRef = useRef<EuiFilePickerClass>(null);
4351

@@ -170,7 +178,18 @@ export const LookupsFileUpload = React.memo<LookupsFileUploadProps>(
170178
</EuiFormRow>
171179
</EuiFlexItem>
172180
<EuiFlexItem>
173-
<EuiFlexGroup justifyContent="flexEnd" gutterSize="none">
181+
<EuiFlexGroup justifyContent="flexEnd" gutterSize="s">
182+
{onSkip && (
183+
<EuiFlexItem grow={false}>
184+
<EuiButtonEmpty
185+
onClick={onSkip}
186+
aria-label={i18n.SKIP_BUTTON_ARIA_LABEL}
187+
data-test-subj="lookupsUploadSkipButton"
188+
>
189+
{i18n.SKIP_BUTTON}
190+
</EuiButtonEmpty>
191+
</EuiFlexItem>
192+
)}
174193
<EuiFlexItem grow={false}>
175194
<UploadFileButton
176195
onClick={createLookups}

x-pack/solutions/security/plugins/security_solution/public/siem_migrations/common/components/migration_steps/lookups/lookups_file_upload/translations.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,13 @@ export const REFERENCE_SETS_DATA_INPUT_FILE_UPLOAD_LABEL = i18n.translate(
2626
'xpack.securitySolution.siemMigrations.common.dataInputFlyout.lookups.referenceSetsFileUpload.label',
2727
{ defaultMessage: 'Upload reference set files' }
2828
);
29+
30+
export const SKIP_BUTTON = i18n.translate(
31+
'xpack.securitySolution.siemMigrations.common.dataInputFlyout.lookups.skipButton',
32+
{ defaultMessage: 'Skip' }
33+
);
34+
35+
export const SKIP_BUTTON_ARIA_LABEL = i18n.translate(
36+
'xpack.securitySolution.siemMigrations.common.dataInputFlyout.lookups.skipButtonAriaLabel',
37+
{ defaultMessage: 'Skip this step and continue without uploading all the items' }
38+
);

x-pack/solutions/security/plugins/security_solution/public/siem_migrations/common/components/migration_steps/lookups/missing_lookups_list/translations.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export const REFERENCE_SETS_QRADAR_APP = i18n.translate(
1616
'xpack.securitySolution.siemMigrations.common.dataInputFlyout.lookups.missingReferenceSetsList.qradarAppSection',
1717
{
1818
defaultMessage:
19-
"We've also found reference sets within your rules. To fully translate those rules containing these reference sets, follow the step-by-step guide to export and upload them all.",
19+
'Below are the reference set found in your rules. Export them from QRadar and upload here.',
2020
}
2121
);
2222

x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/data_input_flyout.test.tsx

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import { TestProviders } from '../../../../common/mock/test_providers';
1414
import { SiemMigrationTaskStatus } from '../../../../../common/siem_migrations/constants';
1515
import { getRuleMigrationStatsMock } from '../../__mocks__/migration_rule_stats';
1616
import { useStartRulesMigrationModal } from '../../hooks/use_start_rules_migration_modal';
17+
import { MigrationSource } from '../../../common/types';
18+
import type { HandleMissingResourcesIndexed } from '../../../common/types';
1719

1820
const mockOnClose = jest.fn();
1921
const mockStartMigration = jest.fn();
@@ -35,6 +37,9 @@ jest.mock('../../../../common/lib/kibana/kibana_react', () => ({
3537
api: {
3638
getMissingResources: jest.fn(),
3739
},
40+
telemetry: {
41+
reportSetupLookupNameCopied: jest.fn(),
42+
},
3843
},
3944
},
4045
notifications: {
@@ -85,13 +90,36 @@ jest.mock('../../logic/use_start_migration', () => {
8590
};
8691
});
8792

93+
jest.mock('../../../../common/hooks/use_experimental_features', () => ({
94+
useIsExperimentalFeatureEnabled: jest.fn(() => true),
95+
}));
96+
97+
const mockUseMissingResources = jest.fn();
98+
jest.mock('../../../common/hooks/use_missing_resources', () => ({
99+
useMissingResources: (...args: unknown[]) => mockUseMissingResources(...args),
100+
}));
101+
102+
jest.mock('../../service/hooks/use_enhance_rules', () => ({
103+
useEnhanceRules: () => ({ enhanceRules: jest.fn(), isLoading: false, error: null }),
104+
}));
105+
106+
jest.mock('../../../../common/hooks/use_app_toasts', () => ({
107+
useAppToasts: () => ({ addError: jest.fn(), addSuccess: jest.fn() }),
108+
}));
109+
88110
jest.mock('../../hooks/use_start_rules_migration_modal');
89111
const useStartRulesMigrationModalMock = useStartRulesMigrationModal as jest.MockedFunction<
90112
typeof useStartRulesMigrationModal
91113
>;
92114

93115
describe('MigrationDataInputFlyout', () => {
94116
beforeEach(() => {
117+
mockUseMissingResources.mockReturnValue({
118+
missingResourcesIndexed: undefined,
119+
onMissingResourcesFetched: jest.fn(),
120+
missingResourceCount: 0,
121+
});
122+
95123
useStartRulesMigrationModalMock.mockImplementation(({ onStartMigrationWithSettings }) => {
96124
return {
97125
modal: (
@@ -271,4 +299,68 @@ describe('MigrationDataInputFlyout', () => {
271299
skipPrebuiltRulesMatching: false,
272300
});
273301
});
302+
303+
describe('QRadar skip reference set step', () => {
304+
const MISSING_LOOKUPS = { macros: [], lookups: ['ref_set_1', 'ref_set_2'] };
305+
306+
beforeEach(() => {
307+
const react = jest.requireActual('react');
308+
mockUseMissingResources.mockImplementation(
309+
({
310+
handleMissingResourcesIndexed,
311+
migrationSource,
312+
}: {
313+
handleMissingResourcesIndexed?: HandleMissingResourcesIndexed;
314+
migrationSource: MigrationSource;
315+
}) => {
316+
react.useEffect(() => {
317+
handleMissingResourcesIndexed?.({
318+
migrationSource,
319+
newMissingResourcesIndexed: MISSING_LOOKUPS,
320+
});
321+
}, [handleMissingResourcesIndexed, migrationSource]);
322+
323+
return {
324+
missingResourcesIndexed: MISSING_LOOKUPS,
325+
onMissingResourcesFetched: jest.fn(),
326+
missingResourceCount: MISSING_LOOKUPS.lookups.length,
327+
};
328+
}
329+
);
330+
});
331+
332+
const qradarMigrationStats = getRuleMigrationStatsMock({
333+
id: 'qradar-migration-1',
334+
status: SiemMigrationTaskStatus.READY,
335+
vendor: MigrationSource.QRADAR,
336+
});
337+
338+
it('advances from Reference Set step to Enhancements step when skip button is clicked', async () => {
339+
const { getByTestId, queryByTestId } = render(
340+
<TestProviders>
341+
<MigrationDataInputFlyout onClose={jest.fn()} migrationStats={qradarMigrationStats} />
342+
</TestProviders>
343+
);
344+
345+
await waitFor(() => {
346+
expect(getByTestId('lookupsUploadSkipButton')).toBeInTheDocument();
347+
});
348+
349+
expect(getByTestId('referenceSetsUploadStepNumber')).toHaveTextContent('Current step is 2');
350+
expect(getByTestId('enhancementsStepNumber')).toHaveTextContent('3');
351+
expect(queryByTestId('enhancementTypeSelect')).not.toBeInTheDocument();
352+
353+
fireEvent.click(getByTestId('lookupsUploadSkipButton'));
354+
355+
await waitFor(() => {
356+
expect(getByTestId('enhancementsStepNumber')).toHaveTextContent('Current step is 3');
357+
});
358+
359+
expect(getByTestId('enhancementTypeSelect')).toBeInTheDocument();
360+
expect(getByTestId('enhancementFilePicker')).toBeInTheDocument();
361+
362+
expect(queryByTestId('lookupsUploadSkipButton')).not.toBeInTheDocument();
363+
expect(queryByTestId('referenceSetsUploadDescription')).not.toBeInTheDocument();
364+
});
365+
});
274366
});

x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/reference_set/reference_set_data_input.test.tsx

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* 2.0.
66
*/
77

8-
import { render } from '@testing-library/react';
8+
import { fireEvent, render } from '@testing-library/react';
99
import React from 'react';
1010
import { ReferenceSetDataInput } from './reference_set_data_input';
1111
import { getRuleMigrationStatsMock } from '../../../../__mocks__';
@@ -81,7 +81,7 @@ describe('ReferenceSetDataInput', () => {
8181
);
8282
expect(getByTestId('referenceSetsUploadDescription')).toBeInTheDocument();
8383
expect(getByTestId('referenceSetsUploadDescription')).toHaveTextContent(
84-
`We've also found reference sets within your rules. To fully translate those rules containing these reference sets, follow the step-by-step guide to export and upload them all.`
84+
`We've also found reference sets within your rules. To fully translate those rules containing these reference sets, upload them all`
8585
);
8686
});
8787

@@ -111,4 +111,33 @@ describe('ReferenceSetDataInput', () => {
111111
);
112112
expect(queryByTestId('referenceSetsUploadDescription')).not.toBeInTheDocument();
113113
});
114+
115+
it('renders skip button when step is current', () => {
116+
const { getByTestId } = render(
117+
<TestProviders>
118+
<ReferenceSetDataInput {...defaultProps} />
119+
</TestProviders>
120+
);
121+
expect(getByTestId('lookupsUploadSkipButton')).toBeInTheDocument();
122+
expect(getByTestId('lookupsUploadSkipButton')).toHaveTextContent('Skip');
123+
});
124+
125+
it('calls setDataInputStep with Enhancements when skip button is clicked', () => {
126+
const { getByTestId } = render(
127+
<TestProviders>
128+
<ReferenceSetDataInput {...defaultProps} />
129+
</TestProviders>
130+
);
131+
fireEvent.click(getByTestId('lookupsUploadSkipButton'));
132+
expect(defaultProps.setDataInputStep).toHaveBeenCalledWith(QradarDataInputStep.Enhancements);
133+
});
134+
135+
it('does not render skip button when step is not current', () => {
136+
const { queryByTestId } = render(
137+
<TestProviders>
138+
<ReferenceSetDataInput {...defaultProps} dataInputStep={SplunkDataInputStep.Upload} />
139+
</TestProviders>
140+
);
141+
expect(queryByTestId('lookupsUploadSkipButton')).not.toBeInTheDocument();
142+
});
114143
});

x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/reference_set/reference_set_data_input.tsx

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import { SubSteps } from '../../../../../common/components';
2323
import { getEuiStepStatus } from '../../../../../common/utils/get_eui_step_status';
2424
import { useKibana } from '../../../../../../common/lib/kibana/kibana_react';
2525
import type { RuleMigrationTaskStats } from '../../../../../../../common/siem_migrations/model/rule_migration.gen';
26-
import { QradarDataInputStep, type OnResourcesCreated } from '../../types';
26+
import { QradarDataInputStep } from '../../types';
2727
import * as i18n from './translations';
2828
import { useMissingReferenceSetsListStep } from './sub_steps/missing_reference_set_list';
2929
import { useReferencesFileUploadStep } from './sub_steps/reference_sets_file_upload';
@@ -32,7 +32,7 @@ import { type MigrationStepProps } from '../../../../../common/types';
3232
interface ReferenceSetDataInputSubStepsProps {
3333
migrationStats: RuleMigrationTaskStats;
3434
missingReferenceSet: string[];
35-
onAllReferenceSetCreated: OnResourcesCreated;
35+
onComplete: () => void;
3636
}
3737

3838
export const ReferenceSetDataInput = React.memo<MigrationStepProps>(
@@ -41,15 +41,16 @@ export const ReferenceSetDataInput = React.memo<MigrationStepProps>(
4141
() => missingResourcesIndexed?.lookups,
4242
[missingResourcesIndexed]
4343
);
44-
const onAllReferenceSetCreated = useCallback(() => {
45-
setDataInputStep(QradarDataInputStep.Enhancements);
46-
}, [setDataInputStep]);
4744

4845
const dataInputStatus = useMemo(
4946
() => getEuiStepStatus(QradarDataInputStep.ReferenceSet, dataInputStep),
5047
[dataInputStep]
5148
);
5249

50+
const onComplete = useCallback(() => {
51+
setDataInputStep(QradarDataInputStep.Enhancements);
52+
}, [setDataInputStep]);
53+
5354
return (
5455
<EuiPanel hasShadow={false} hasBorder>
5556
<EuiFlexGroup direction="column">
@@ -81,7 +82,7 @@ export const ReferenceSetDataInput = React.memo<MigrationStepProps>(
8182
<ReferenceSetDataInputSubSteps
8283
migrationStats={migrationStats}
8384
missingReferenceSet={missingReferenceSet}
84-
onAllReferenceSetCreated={onAllReferenceSetCreated}
85+
onComplete={onComplete}
8586
/>
8687
</EuiFlexItem>
8788
</>
@@ -96,7 +97,7 @@ ReferenceSetDataInput.displayName = 'ReferenceSetDataInput';
9697
const END = 10 as const;
9798
type SubStep = 1 | 2 | typeof END;
9899
export const ReferenceSetDataInputSubSteps = React.memo<ReferenceSetDataInputSubStepsProps>(
99-
({ migrationStats, missingReferenceSet, onAllReferenceSetCreated }) => {
100+
({ migrationStats, missingReferenceSet, onComplete }) => {
100101
const { telemetry } = useKibana().services.siemMigrations.rules;
101102
const [subStep, setSubStep] = useState<SubStep>(1);
102103
const [uploadedLookups, setUploadedLookups] = useState<UploadedLookups>({});
@@ -111,9 +112,9 @@ export const ReferenceSetDataInputSubSteps = React.memo<ReferenceSetDataInputSub
111112
useEffect(() => {
112113
if (missingReferenceSet.every((referenceSet) => uploadedLookups[referenceSet] != null)) {
113114
setSubStep(END);
114-
onAllReferenceSetCreated();
115+
onComplete();
115116
}
116-
}, [uploadedLookups, missingReferenceSet, onAllReferenceSetCreated]);
117+
}, [uploadedLookups, missingReferenceSet, onComplete]);
117118

118119
// Copy query step
119120
const onCopied = useCallback(() => {
@@ -139,6 +140,7 @@ export const ReferenceSetDataInputSubSteps = React.memo<ReferenceSetDataInputSub
139140
migrationStats,
140141
missingLookups: missingReferenceSet,
141142
addUploadedLookups,
143+
onSkip: onComplete,
142144
});
143145

144146
const steps = useMemo<EuiStepProps[]>(() => [copyStep, uploadStep], [copyStep, uploadStep]);

x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/reference_set/sub_steps/reference_sets_file_upload/index.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,13 @@ export interface RulesFileUploadStepProps {
2020
migrationStats: RuleMigrationTaskStats;
2121
missingLookups: string[];
2222
addUploadedLookups: AddUploadedLookups;
23+
onSkip?: () => void;
2324
}
2425
export const useReferencesFileUploadStep = ({
2526
status,
2627
migrationStats,
2728
addUploadedLookups,
29+
onSkip,
2830
}: RulesFileUploadStepProps): EuiStepProps => {
2931
const { upsertResources, isLoading, error } = useUpsertResources(addUploadedLookups);
3032

@@ -61,6 +63,7 @@ export const useReferencesFileUploadStep = ({
6163
isLoading={isLoading}
6264
apiError={error?.message}
6365
migrationSource={MigrationSource.QRADAR}
66+
onSkip={onSkip}
6467
/>
6568
),
6669
};

x-pack/solutions/security/plugins/security_solution/public/siem_migrations/rules/components/data_input_flyout/steps/reference_set/translations.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,6 @@ export const REFERENCE_SET_DATA_INPUT_TITLE = i18n.translate(
1414
export const REFERENCE_SET_DATA_INPUT_DESCRIPTION = i18n.translate(
1515
'xpack.securitySolution.siemMigrations.rules.dataInputFlyout.referenceSet.description',
1616
{
17-
defaultMessage: `We've also found reference sets within your rules. To fully translate those rules containing these reference sets, follow the step-by-step guide to export and upload them all.`,
17+
defaultMessage: `We've also found reference sets within your rules. To fully translate those rules containing these reference sets, upload them all.`,
1818
}
1919
);

0 commit comments

Comments
 (0)