Wizard: Add FIPS mode support for OpenSCAP and compliance profiles (HMS-8919)

Automatically enable FIPS mode when:

User selects OpenSCAP profile with FIPS enabled (e.g., DISA STIG)
User selects compliance profile with FIPS enabled and not customized off

- Add FIPS checkbox in openscap step
- Display FIPS status in review step
- Add unit tests to FIPS checkbox feature
This ensures security compliance for profiles that require FIPS mode
without manual user intervention.
This commit is contained in:
Michal Gold 2025-08-03 18:43:02 +03:00 committed by Gianluca Zuccarelli
parent 3461c908fb
commit d66f54a847
6 changed files with 195 additions and 0 deletions

View file

@ -17,6 +17,7 @@ import {
selectCompliancePolicyID,
selectComplianceProfileID,
selectDistribution,
selectFips,
} from '../../../../../store/wizardSlice';
type OscapProfileInformationOptionPropType = {
@ -30,6 +31,7 @@ export const OscapProfileInformation = ({
const release = useAppSelector(selectDistribution);
const compliancePolicyID = useAppSelector(selectCompliancePolicyID);
const complianceProfileID = useAppSelector(selectComplianceProfileID);
const fips = useAppSelector(selectFips);
const {
data: oscapProfileInfo,
@ -159,6 +161,19 @@ export const OscapProfileInformation = ({
</CodeBlockCode>
</CodeBlock>
</Content>
<Content
component={ContentVariants.dt}
className='pf-v5-u-min-width'
>
FIPS mode
</Content>
<Content component={ContentVariants.dd}>
<CodeBlock>
<CodeBlockCode>
{fips.enabled ? 'Enabled' : 'Disabled'}
</CodeBlockCode>
</CodeBlock>
</Content>
</Content>
</>
)}

View file

@ -22,6 +22,7 @@ import {
import {
changeCompliance,
changeFileSystemConfigurationType,
changeFips,
clearKernelAppend,
selectCompliancePolicyID,
selectCompliancePolicyTitle,
@ -143,6 +144,7 @@ const PolicySelector = () => {
dispatch(changeFileSystemConfigurationType('automatic'));
handleServices(undefined);
dispatch(clearKernelAppend());
dispatch(changeFips(false));
};
const applyChanges = (selection: ComplianceSelectOptionValueType) => {
@ -177,6 +179,7 @@ const PolicySelector = () => {
policyTitle: selection.title,
}),
);
dispatch(changeFips(response?.fips?.enabled || false));
});
}
};

View file

@ -33,6 +33,7 @@ import {
import {
changeCompliance,
changeFileSystemConfigurationType,
changeFips,
clearKernelAppend,
selectComplianceProfileID,
selectComplianceType,
@ -181,6 +182,7 @@ const ProfileSelector = () => {
dispatch(changeFileSystemConfigurationType('automatic'));
handleServices(undefined);
dispatch(clearKernelAppend());
dispatch(changeFips(false));
setInputValue('');
setFilterValue('');
};
@ -261,6 +263,7 @@ const ProfileSelector = () => {
policyTitle: undefined,
}),
);
dispatch(changeFips(response?.fips?.enabled || false));
});
}
};

View file

@ -3,8 +3,10 @@ import React, { useEffect } from 'react';
import {
Alert,
AlertActionLink,
Checkbox,
Content,
Form,
FormGroup,
Title,
ToggleGroup,
ToggleGroupItem,
@ -29,6 +31,7 @@ import {
changeDisabledServices,
changeEnabledServices,
changeFileSystemConfigurationType,
changeFips,
changeMaskedServices,
clearKernelAppend,
ComplianceType,
@ -36,6 +39,7 @@ import {
selectComplianceProfileID,
selectComplianceType,
selectDistribution,
selectFips,
} from '../../../../store/wizardSlice';
import { useFlag } from '../../../../Utilities/useGetEnvironment';
import { useOnPremOpenSCAPAvailable } from '../../../../Utilities/useOnPremOpenSCAP';
@ -46,6 +50,7 @@ const OscapContent = () => {
const complianceEnabled = useFlag('image-builder.compliance.enabled');
const complianceType = useAppSelector(selectComplianceType);
const profileID = useAppSelector(selectComplianceProfileID);
const fips = useAppSelector(selectFips);
const prefetchOscapProfile = useBackendPrefetch('getOscapProfiles', {});
const release = removeBetaFromRelease(useAppSelector(selectDistribution));
const majorVersion = release.split('-')[1];
@ -66,6 +71,10 @@ const OscapContent = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const handleFipsToggle = (checked: boolean) => {
dispatch(changeFips(checked));
};
const handleTypeChange = (complianceType: string) => {
dispatch(changeComplianceType(complianceType as ComplianceType));
@ -87,6 +96,7 @@ const OscapContent = () => {
dispatch(changeMaskedServices([]));
dispatch(changeDisabledServices([]));
dispatch(clearKernelAppend());
dispatch(changeFips(false));
};
if (!process.env.IS_ON_PREMISE) {
@ -117,6 +127,15 @@ const OscapContent = () => {
versions. This will automatically help monitor the adherence of your
registered RHEL systems to a selected policy or profile.
</Content>
<FormGroup>
<Checkbox
id='fips-enabled-checkbox'
label='Enable FIPS mode'
isChecked={fips.enabled}
onChange={(_event, checked) => handleFipsToggle(checked)}
description='Enable FIPS 140-2 compliant cryptographic algorithms. This setting will be applied at build time and will persist on boot.'
/>
</FormGroup>
{complianceEnabled && (
<ToggleGroup aria-label='Default with single selectable'>
<ToggleGroupItem

View file

@ -0,0 +1,152 @@
import { screen, waitFor } from '@testing-library/react';
import { userEvent } from '@testing-library/user-event';
import { CREATE_BLUEPRINT } from '../../../../../constants';
import { CreateBlueprintRequest } from '../../../../../store/imageBuilderApi';
import { baseCreateBlueprintRequest } from '../../../../fixtures/editMode';
import {
clickNext,
clickRegisterLater,
enterBlueprintName,
interceptBlueprintRequest,
openAndDismissSaveAndBuildModal,
renderCreateMode,
selectRhel9,
} from '../../wizardTestUtils';
vi.mock('@unleash/proxy-client-react', () => ({
useUnleashContext: () => vi.fn(),
useFlag: vi.fn((flag) => {
switch (flag) {
case 'image-builder.compliance.enabled':
return true;
default:
return false;
}
}),
}));
const goToComplianceStep = async () => {
const user = userEvent.setup();
await selectRhel9();
const guestImageCheckBox = await screen.findByRole('checkbox', {
name: /virtualization guest image checkbox/i,
});
await waitFor(() => user.click(guestImageCheckBox));
await clickNext(); // Registration
await clickRegisterLater();
await clickNext(); // Compliance
await screen.findByRole('heading', { name: /Compliance/ });
const button = await screen.findByRole('button', {
name: /Compliance policies/,
});
await waitFor(() => user.click(button));
await screen.findByText('None');
};
const selectStigPolicy = async () => {
const user = userEvent.setup();
const policyMenu = await screen.findByText('None');
await waitFor(() => user.click(policyMenu));
const stigPolicy = await screen.findByRole('option', {
name: /stig gui/i,
});
await waitFor(() => user.click(stigPolicy));
const profile_id = await screen.findByTestId('oscap-profile-info-ref-id');
expect(profile_id).toHaveTextContent('content_profile_stig_gui');
};
const getFipsCheckbox = async () => {
return await screen.findByRole('checkbox', {
name: /enable fips mode/i,
});
};
const toggleFipsCheckbox = async () => {
const user = userEvent.setup();
const fipsCheckbox = await getFipsCheckbox();
await waitFor(() => user.click(fipsCheckbox));
return fipsCheckbox;
};
const goToReviewStep = async () => {
await clickNext(); // File system configuration
await clickNext(); // Snapshot repositories
await clickNext(); // Custom repositories
await clickNext(); // Additional packages
await clickNext(); // Users
await clickNext(); // Timezone
await clickNext(); // Locale
await clickNext(); // Hostname
await clickNext(); // Kernel
await clickNext(); // Firewall
await clickNext(); // Services
await clickNext(); // FirstBoot
await clickNext(); // Details
await enterBlueprintName('FIPS test');
await clickNext(); // Review
};
describe('FIPS Mode Tests', () => {
beforeEach(() => {
vi.clearAllMocks();
});
test('FIPS checkbox is present and functional', async () => {
await renderCreateMode();
await goToComplianceStep();
const fipsCheckbox = await getFipsCheckbox();
expect(fipsCheckbox).toBeInTheDocument();
expect(fipsCheckbox).not.toBeChecked();
await toggleFipsCheckbox();
expect(fipsCheckbox).toBeChecked();
await screen.findByText(
/enable fips 140-2 compliant cryptographic algorithms/i,
);
});
test('FIPS checkbox is automatically enabled when selecting STIG GUI profile', async () => {
await renderCreateMode();
await goToComplianceStep();
const fipsCheckbox = await getFipsCheckbox();
expect(fipsCheckbox).not.toBeChecked();
await selectStigPolicy();
await waitFor(() => {
expect(fipsCheckbox).toBeChecked();
});
});
test('FIPS setting included in blueprint when manually enabled', async () => {
await renderCreateMode();
await goToComplianceStep();
await toggleFipsCheckbox();
await goToReviewStep();
await openAndDismissSaveAndBuildModal();
const receivedRequest = await interceptBlueprintRequest(CREATE_BLUEPRINT);
const expectedRequest: CreateBlueprintRequest = {
...baseCreateBlueprintRequest,
name: 'FIPS test',
customizations: {
...baseCreateBlueprintRequest.customizations,
fips: {
enabled: true,
},
},
};
await waitFor(() => {
expect(receivedRequest).toEqual(expectedRequest);
});
});
});

View file

@ -113,6 +113,9 @@ export const oscapCustomizations = (
masked: ['nfs-server', 'rpcbind', 'autofs', 'nftables'],
enabled: ['crond', 'firewalld', 'systemd-journald', 'rsyslog', 'auditd'],
},
fips: {
enabled: true,
},
};
};