Wizard: Add kernel append input
This adds the kernel append input. New arguments can be added by pressing the "Add" button or hitting Enter after the argument. The kernel arguments linked to a selected OpenSCAP profile are rendered in a category marked as "Required by OpenSCAP" and are read only.
This commit is contained in:
parent
199dd3d5d7
commit
1b21852518
13 changed files with 305 additions and 40 deletions
|
|
@ -19,6 +19,7 @@ type ChippingInputProps = {
|
|||
ariaLabel: string;
|
||||
placeholder: string;
|
||||
validator: (value: string) => boolean;
|
||||
requiredList?: string[] | undefined;
|
||||
list: string[] | undefined;
|
||||
item: string;
|
||||
addAction: (value: string) => UnknownAction;
|
||||
|
|
@ -30,6 +31,7 @@ const ChippingInput = ({
|
|||
placeholder,
|
||||
validator,
|
||||
list,
|
||||
requiredList,
|
||||
item,
|
||||
addAction,
|
||||
removeAction,
|
||||
|
|
@ -64,7 +66,7 @@ const ChippingInput = ({
|
|||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent, value: string) => {
|
||||
if (e.key === ' ' || e.key === 'Enter' || e.key === ',') {
|
||||
if (e.key === ' ' || e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
addItem(value);
|
||||
}
|
||||
|
|
@ -112,7 +114,24 @@ const ChippingInput = ({
|
|||
<HelperTextItem variant={'error'}>{errorText}</HelperTextItem>
|
||||
</HelperText>
|
||||
)}
|
||||
<ChipGroup numChips={5} className="pf-v5-u-mt-sm pf-v5-u-w-100">
|
||||
{requiredList && requiredList.length > 0 && (
|
||||
<ChipGroup
|
||||
categoryName="Required by OpenSCAP"
|
||||
numChips={20}
|
||||
className="pf-v5-u-mt-sm pf-v5-u-w-100"
|
||||
>
|
||||
{requiredList.map((item) => (
|
||||
<Chip
|
||||
key={item}
|
||||
onClick={() => dispatch(removeAction(item))}
|
||||
isReadOnly
|
||||
>
|
||||
{item}
|
||||
</Chip>
|
||||
))}
|
||||
</ChipGroup>
|
||||
)}
|
||||
<ChipGroup numChips={20} className="pf-v5-u-mt-sm pf-v5-u-w-100">
|
||||
{list?.map((item) => (
|
||||
<Chip key={item} onClick={() => dispatch(removeAction(item))}>
|
||||
{item}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,57 @@ import React from 'react';
|
|||
|
||||
import { FormGroup } from '@patternfly/react-core';
|
||||
|
||||
import { useAppSelector } from '../../../../../store/hooks';
|
||||
import { useGetOscapCustomizationsQuery } from '../../../../../store/imageBuilderApi';
|
||||
import {
|
||||
addKernelArg,
|
||||
removeKernelArg,
|
||||
selectComplianceProfileID,
|
||||
selectDistribution,
|
||||
selectKernel,
|
||||
} from '../../../../../store/wizardSlice';
|
||||
import ChippingInput from '../../../ChippingInput';
|
||||
import { isKernelArgumentValid } from '../../../validators';
|
||||
|
||||
const KernelArguments = () => {
|
||||
return <FormGroup isRequired={false} label="Append"></FormGroup>;
|
||||
const kernelAppend = useAppSelector(selectKernel).append;
|
||||
|
||||
const release = useAppSelector(selectDistribution);
|
||||
const complianceProfileID = useAppSelector(selectComplianceProfileID);
|
||||
|
||||
const { data: oscapProfileInfo } = useGetOscapCustomizationsQuery(
|
||||
{
|
||||
distribution: release,
|
||||
// @ts-ignore if complianceProfileID is undefined the query is going to get skipped, so it's safe here to ignore the linter here
|
||||
profile: complianceProfileID,
|
||||
},
|
||||
{
|
||||
skip: !complianceProfileID,
|
||||
}
|
||||
);
|
||||
|
||||
const requiredByOpenSCAP = kernelAppend.filter((arg) =>
|
||||
oscapProfileInfo?.kernel?.append?.split(' ').includes(arg)
|
||||
);
|
||||
|
||||
const notRequiredByOpenSCAP = kernelAppend.filter(
|
||||
(arg) => !oscapProfileInfo?.kernel?.append?.split(' ').includes(arg)
|
||||
);
|
||||
|
||||
return (
|
||||
<FormGroup isRequired={false} label="Append">
|
||||
<ChippingInput
|
||||
ariaLabel="Add kernel argument"
|
||||
placeholder="Add kernel argument"
|
||||
validator={isKernelArgumentValid}
|
||||
list={notRequiredByOpenSCAP}
|
||||
requiredList={requiredByOpenSCAP}
|
||||
item="Kernel argument"
|
||||
addAction={addKernelArg}
|
||||
removeAction={removeKernelArg}
|
||||
/>
|
||||
</FormGroup>
|
||||
);
|
||||
};
|
||||
|
||||
export default KernelArguments;
|
||||
|
|
|
|||
|
|
@ -51,8 +51,9 @@ import {
|
|||
changeEnabledServices,
|
||||
changeMaskedServices,
|
||||
changeDisabledServices,
|
||||
changeKernelAppend,
|
||||
selectComplianceType,
|
||||
clearKernelAppend,
|
||||
addKernelArg,
|
||||
} from '../../../../store/wizardSlice';
|
||||
import { useHasSpecificTargetOnly } from '../../utilities/hasSpecificTargetOnly';
|
||||
import { parseSizeUnit } from '../../utilities/parseSizeUnit';
|
||||
|
|
@ -174,7 +175,7 @@ const ProfileSelector = () => {
|
|||
clearOscapPackages(currentProfileData?.packages || []);
|
||||
dispatch(changeFileSystemConfigurationType('automatic'));
|
||||
handleServices(undefined);
|
||||
dispatch(changeKernelAppend(''));
|
||||
dispatch(clearKernelAppend());
|
||||
};
|
||||
|
||||
const handlePackages = (
|
||||
|
|
@ -228,6 +229,17 @@ const ProfileSelector = () => {
|
|||
dispatch(changeDisabledServices(services?.disabled || []));
|
||||
};
|
||||
|
||||
const handleKernelAppend = (kernelAppend: string | undefined) => {
|
||||
dispatch(clearKernelAppend());
|
||||
|
||||
if (kernelAppend) {
|
||||
const kernelArgsArray = kernelAppend.split(' ');
|
||||
for (const arg in kernelArgsArray) {
|
||||
dispatch(addKernelArg(kernelArgsArray[arg]));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelect = (
|
||||
_event: React.MouseEvent<Element, MouseEvent>,
|
||||
selection: OScapSelectOptionValueType | ComplianceSelectOptionValueType
|
||||
|
|
@ -251,7 +263,7 @@ const ProfileSelector = () => {
|
|||
handlePartitions(oscapPartitions);
|
||||
handlePackages(oldOscapPackages, newOscapPackages);
|
||||
handleServices(response.services);
|
||||
dispatch(changeKernelAppend(response.kernel?.append || ''));
|
||||
handleKernelAppend(response.kernel?.append);
|
||||
if (complianceType === 'openscap') {
|
||||
dispatch(
|
||||
changeCompliance({
|
||||
|
|
|
|||
|
|
@ -130,19 +130,6 @@ export const OscapProfileInformation = ({
|
|||
>
|
||||
{oscapProfile?.profile_id}
|
||||
</TextListItem>
|
||||
<TextListItem
|
||||
component={TextListItemVariants.dt}
|
||||
className="pf-v5-u-min-width"
|
||||
>
|
||||
Kernel arguments:
|
||||
</TextListItem>
|
||||
<TextListItem component={TextListItemVariants.dd}>
|
||||
<CodeBlock>
|
||||
<CodeBlockCode>
|
||||
{oscapProfileInfo?.kernel?.append}
|
||||
</CodeBlockCode>
|
||||
</CodeBlock>
|
||||
</TextListItem>
|
||||
<TextListItem
|
||||
component={TextListItemVariants.dt}
|
||||
className="pf-v5-u-min-width"
|
||||
|
|
|
|||
|
|
@ -31,9 +31,9 @@ import {
|
|||
changeDisabledServices,
|
||||
removePackage,
|
||||
changeFileSystemConfigurationType,
|
||||
changeKernelAppend,
|
||||
selectDistribution,
|
||||
selectComplianceType,
|
||||
clearKernelAppend,
|
||||
} from '../../../../store/wizardSlice';
|
||||
import { useGetEnvironment } from '../../../../Utilities/useGetEnvironment';
|
||||
|
||||
|
|
@ -84,7 +84,7 @@ const OscapStep = () => {
|
|||
dispatch(changeEnabledServices([]));
|
||||
dispatch(changeMaskedServices([]));
|
||||
dispatch(changeDisabledServices([]));
|
||||
dispatch(changeKernelAppend(''));
|
||||
dispatch(clearKernelAppend());
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -336,7 +336,7 @@ function commonRequestToState(
|
|||
},
|
||||
kernel: {
|
||||
name: request.customizations.kernel?.name || '',
|
||||
append: request.customizations?.kernel?.append || '',
|
||||
append: request.customizations?.kernel?.append?.split(' ') || [],
|
||||
},
|
||||
timezone: {
|
||||
timezone: request.customizations.timezone?.timezone || '',
|
||||
|
|
@ -763,13 +763,25 @@ const getPayloadRepositories = (state: RootState) => {
|
|||
|
||||
const getKernel = (state: RootState) => {
|
||||
const kernel = selectKernel(state);
|
||||
const kernelAppendString = selectKernel(state).append.join(' ');
|
||||
|
||||
if (!kernel.name && !kernel.append) {
|
||||
const kernelRequest = {};
|
||||
|
||||
if (!kernel.name && kernel.append.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
name: selectKernel(state).name || undefined,
|
||||
append: selectKernel(state).append || undefined,
|
||||
};
|
||||
if (kernel.name) {
|
||||
Object.assign(kernelRequest, {
|
||||
name: kernel.name,
|
||||
});
|
||||
}
|
||||
|
||||
if (kernelAppendString !== '') {
|
||||
Object.assign(kernelRequest, {
|
||||
append: kernelAppendString,
|
||||
});
|
||||
}
|
||||
|
||||
return Object.keys(kernelRequest).length > 0 ? kernelRequest : undefined;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -124,6 +124,14 @@ export const isKernelNameValid = (kernelName: string) => {
|
|||
);
|
||||
};
|
||||
|
||||
export const isKernelArgumentValid = (arg: string) => {
|
||||
if (!arg) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return /^[a-zA-Z0-9=-_,"']*$/.test(arg);
|
||||
};
|
||||
|
||||
export const isPortValid = (port: string) => {
|
||||
return /^(\d{1,5}|[a-z]{1,6})(-\d{1,5})?:[a-z]{1,6}$/.test(port);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -135,7 +135,7 @@ export type wizardState = {
|
|||
};
|
||||
kernel: {
|
||||
name: string;
|
||||
append: string;
|
||||
append: string[];
|
||||
};
|
||||
locale: Locale;
|
||||
details: {
|
||||
|
|
@ -213,7 +213,7 @@ export const initialState: wizardState = {
|
|||
},
|
||||
kernel: {
|
||||
name: '',
|
||||
append: '',
|
||||
append: [],
|
||||
},
|
||||
locale: {
|
||||
languages: [],
|
||||
|
|
@ -817,8 +817,27 @@ export const wizardSlice = createSlice({
|
|||
changeKernelName: (state, action: PayloadAction<string>) => {
|
||||
state.kernel.name = action.payload;
|
||||
},
|
||||
changeKernelAppend: (state, action: PayloadAction<string>) => {
|
||||
state.kernel.append = action.payload;
|
||||
addKernelArg: (state, action: PayloadAction<string>) => {
|
||||
const existingArgIndex = state.kernel.append.findIndex(
|
||||
(arg) => arg === action.payload
|
||||
);
|
||||
|
||||
if (existingArgIndex !== -1) {
|
||||
state.kernel.append[existingArgIndex] = action.payload;
|
||||
} else {
|
||||
state.kernel.append.push(action.payload);
|
||||
}
|
||||
},
|
||||
removeKernelArg: (state, action: PayloadAction<string>) => {
|
||||
if (state.kernel.append.length > 0) {
|
||||
state.kernel.append.splice(
|
||||
state.kernel.append.findIndex((arg) => arg === action.payload),
|
||||
1
|
||||
);
|
||||
}
|
||||
},
|
||||
clearKernelAppend: (state) => {
|
||||
state.kernel.append = [];
|
||||
},
|
||||
changeTimezone: (state, action: PayloadAction<string>) => {
|
||||
state.timezone.timezone = action.payload;
|
||||
|
|
@ -954,7 +973,9 @@ export const {
|
|||
changeMaskedServices,
|
||||
changeDisabledServices,
|
||||
changeKernelName,
|
||||
changeKernelAppend,
|
||||
addKernelArg,
|
||||
removeKernelArg,
|
||||
clearKernelAppend,
|
||||
changeTimezone,
|
||||
addNtpServer,
|
||||
removeNtpServer,
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ const goToReviewStep = async () => {
|
|||
const addPort = async (port: string) => {
|
||||
const user = userEvent.setup();
|
||||
const portsInput = await screen.findByPlaceholderText(/add port/i);
|
||||
await waitFor(() => user.type(portsInput, port.concat(',')));
|
||||
await waitFor(() => user.type(portsInput, port.concat(' ')));
|
||||
};
|
||||
|
||||
describe('Step Firewall', () => {
|
||||
|
|
|
|||
|
|
@ -39,6 +39,29 @@ const goToKernelStep = async () => {
|
|||
await clickNext(); // Kernel
|
||||
};
|
||||
|
||||
const goToOpenSCAPStep = async () => {
|
||||
const user = userEvent.setup();
|
||||
const guestImageCheckBox = await screen.findByRole('checkbox', {
|
||||
name: /virtualization guest image checkbox/i,
|
||||
});
|
||||
await waitFor(() => user.click(guestImageCheckBox));
|
||||
await clickNext(); // Registration
|
||||
await clickRegisterLater();
|
||||
await clickNext(); // OpenSCAP
|
||||
};
|
||||
|
||||
const goFromOpenSCAPToKernel = async () => {
|
||||
await clickNext(); // File system configuration
|
||||
await clickNext(); // Snapshots
|
||||
await clickNext(); // Custom repositories
|
||||
await clickNext(); // Additional packages
|
||||
await clickNext(); // Users
|
||||
await clickNext(); // Timezone
|
||||
await clickNext(); // Locale
|
||||
await clickNext(); // Hostname
|
||||
await clickNext(); // Kernel
|
||||
};
|
||||
|
||||
const goToReviewStep = async () => {
|
||||
await clickNext(); // Firewall
|
||||
await clickNext(); // Services
|
||||
|
|
@ -77,10 +100,44 @@ const selectCustomKernelName = async (kernelName: string) => {
|
|||
|
||||
const clearKernelName = async () => {
|
||||
const user = userEvent.setup();
|
||||
const kernelNameClearBtn = await screen.findByRole('button', {
|
||||
const kernelNameClearBtn = await screen.findAllByRole('button', {
|
||||
name: /clear input/i,
|
||||
});
|
||||
await waitFor(() => user.click(kernelNameClearBtn));
|
||||
await waitFor(() => user.click(kernelNameClearBtn[0]));
|
||||
};
|
||||
|
||||
const addKernelAppend = async (kernelArg: string) => {
|
||||
const user = userEvent.setup();
|
||||
const kernelAppendInput = await screen.findByPlaceholderText(
|
||||
'Add kernel argument'
|
||||
);
|
||||
await waitFor(() => user.click(kernelAppendInput));
|
||||
await waitFor(() => user.type(kernelAppendInput, kernelArg));
|
||||
|
||||
const addKernelArg = await screen.findByRole('button', {
|
||||
name: /Add kernel argument/,
|
||||
});
|
||||
await waitFor(() => user.click(addKernelArg));
|
||||
};
|
||||
|
||||
const removeKernelArg = async (kernelArg: string) => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
const removeNosmtArgButton = await screen.findByRole('button', {
|
||||
name: `close ${kernelArg}`,
|
||||
});
|
||||
await waitFor(() => user.click(removeNosmtArgButton));
|
||||
};
|
||||
|
||||
const selectProfile = async () => {
|
||||
const user = userEvent.setup();
|
||||
const selectProfileDropdown = await screen.findByRole('textbox', {
|
||||
name: /select a profile/i,
|
||||
});
|
||||
await waitFor(() => user.click(selectProfileDropdown));
|
||||
|
||||
const cis1Profile = await screen.findByText(/Kernel append only profile/i);
|
||||
await waitFor(() => user.click(cis1Profile));
|
||||
};
|
||||
|
||||
describe('Step Kernel', () => {
|
||||
|
|
@ -137,6 +194,31 @@ describe('Step Kernel', () => {
|
|||
expect(screen.queryByText(/Invalid format/)).not.toBeInTheDocument();
|
||||
expect(nextButton).toBeEnabled();
|
||||
});
|
||||
|
||||
test('kernel argument can be added and removed', async () => {
|
||||
await renderCreateMode();
|
||||
await goToKernelStep();
|
||||
await addKernelAppend('nosmt=force');
|
||||
await addKernelAppend('page_poison=1');
|
||||
await removeKernelArg('nosmt=force');
|
||||
expect(screen.queryByText('nosmt=force')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('kernel append from OpenSCAP gets added correctly and cannot be removed', async () => {
|
||||
await renderCreateMode();
|
||||
await goToOpenSCAPStep();
|
||||
await selectProfile();
|
||||
await goFromOpenSCAPToKernel();
|
||||
await screen.findByText('Required by OpenSCAP');
|
||||
await screen.findByText('audit_backlog_limit=8192');
|
||||
await screen.findByText('audit=1');
|
||||
expect(
|
||||
screen.queryByRole('button', { name: /close audit_backlog_limit=8192/i })
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByRole('button', { name: /close audit=1/i })
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Kernel request generated correctly', () => {
|
||||
|
|
@ -184,9 +266,72 @@ describe('Kernel request generated correctly', () => {
|
|||
expect(receivedRequest).toEqual(expectedRequest);
|
||||
});
|
||||
});
|
||||
|
||||
test('with kernel arg added and removed', async () => {
|
||||
await renderCreateMode();
|
||||
await goToKernelStep();
|
||||
await addKernelAppend('nosmt=force');
|
||||
await removeKernelArg('nosmt=force');
|
||||
await goToReviewStep();
|
||||
const receivedRequest = await interceptBlueprintRequest(CREATE_BLUEPRINT);
|
||||
|
||||
const expectedRequest = {
|
||||
...blueprintRequest,
|
||||
customizations: {},
|
||||
};
|
||||
|
||||
await waitFor(() => {
|
||||
expect(receivedRequest).toEqual(expectedRequest);
|
||||
});
|
||||
});
|
||||
|
||||
test('with kernel append', async () => {
|
||||
await renderCreateMode();
|
||||
await goToKernelStep();
|
||||
await addKernelAppend('nosmt=force');
|
||||
await goToReviewStep();
|
||||
const receivedRequest = await interceptBlueprintRequest(CREATE_BLUEPRINT);
|
||||
|
||||
const expectedRequest = {
|
||||
...blueprintRequest,
|
||||
customizations: {
|
||||
kernel: {
|
||||
append: 'nosmt=force',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
await waitFor(() => {
|
||||
expect(receivedRequest).toEqual(expectedRequest);
|
||||
});
|
||||
});
|
||||
|
||||
test('with OpenSCAP profile that includes kernel append', async () => {
|
||||
await renderCreateMode();
|
||||
await goToOpenSCAPStep();
|
||||
await selectProfile();
|
||||
await goFromOpenSCAPToKernel();
|
||||
await goToReviewStep();
|
||||
const receivedRequest = await interceptBlueprintRequest(CREATE_BLUEPRINT);
|
||||
|
||||
const expectedRequest = {
|
||||
...blueprintRequest,
|
||||
customizations: {
|
||||
filesystem: [{ min_size: 10737418240, mountpoint: '/' }],
|
||||
openscap: {
|
||||
profile_id: 'xccdf_org.ssgproject.content_profile_ccn_basic',
|
||||
},
|
||||
kernel: {
|
||||
append: 'audit_backlog_limit=8192 audit=1',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
await waitFor(() => {
|
||||
expect(receivedRequest).toEqual(expectedRequest);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// TO DO 'Kernel step' -> 'revisit step button on Review works'
|
||||
// TO DO 'Kernel request generated correctly' -> 'with valid kernel append'
|
||||
// TO DO 'Kernel request generated correctly' -> 'with valid kernel name and kernel append'
|
||||
// TO DO 'Kernel edit mode'
|
||||
|
|
|
|||
|
|
@ -161,8 +161,6 @@ describe('Step OpenSCAP', () => {
|
|||
await goToOscapStep();
|
||||
await selectProfile();
|
||||
|
||||
await screen.findByText(/kernel arguments:/i);
|
||||
await screen.findByText(/audit_backlog_limit=8192 audit=1/i);
|
||||
await screen.findByText(/disabled services:/i);
|
||||
await screen.findByText(
|
||||
/rpcbind autofs nftables nfs-server emacs-service/i
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ const addNtpServerViaKeyDown = async (ntpServer: string) => {
|
|||
const ntpServersInput = await screen.findByPlaceholderText(
|
||||
/add ntp servers/i
|
||||
);
|
||||
await waitFor(() => user.type(ntpServersInput, ntpServer.concat(',')));
|
||||
await waitFor(() => user.type(ntpServersInput, ntpServer.concat(' ')));
|
||||
};
|
||||
|
||||
const addNtpServerViaAddButton = async (ntpServer: string) => {
|
||||
|
|
|
|||
14
src/test/fixtures/oscap.ts
vendored
14
src/test/fixtures/oscap.ts
vendored
|
|
@ -9,6 +9,7 @@ export const distributionOscapProfiles = (): GetOscapProfilesApiResponse => {
|
|||
'xccdf_org.ssgproject.content_profile_cis_workstation_l2',
|
||||
'xccdf_org.ssgproject.content_profile_standard',
|
||||
'xccdf_org.ssgproject.content_profile_stig_gui',
|
||||
'xccdf_org.ssgproject.content_profile_ccn_basic',
|
||||
];
|
||||
};
|
||||
|
||||
|
|
@ -75,6 +76,19 @@ export const oscapCustomizations = (
|
|||
},
|
||||
};
|
||||
}
|
||||
if (profile === 'xccdf_org.ssgproject.content_profile_ccn_basic') {
|
||||
return {
|
||||
openscap: {
|
||||
profile_id: 'content_profile_ccn_basic',
|
||||
profile_name: 'Kernel append only profile',
|
||||
profile_description:
|
||||
'This is a mocked profile description. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean posuere velit enim, tincidunt porttitor nisl elementum eu.',
|
||||
},
|
||||
kernel: {
|
||||
append: 'audit_backlog_limit=8192 audit=1',
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
filesystem: [{ min_size: 1073741824, mountpoint: '/tmp' }],
|
||||
openscap: {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue