From e43357bf55f29d367bb5fba4d58b286b0b396c83 Mon Sep 17 00:00:00 2001 From: Sanne Raymaekers Date: Wed, 11 Sep 2024 12:11:37 +0200 Subject: [PATCH] Wizard: add compliance step The Insights compliance support reuses most of the existing OpenSCAP step. Depending on the state of the feature flag, it will show radio buttons allowing users to switch between regular openscap and Insights compliance. --- .../CreateImageWizard/steps/Oscap/Oscap.tsx | 297 +++++++++++++----- .../CreateImageWizard/steps/Oscap/index.tsx | 119 +++++-- src/constants.ts | 4 + 3 files changed, 326 insertions(+), 94 deletions(-) diff --git a/src/Components/CreateImageWizard/steps/Oscap/Oscap.tsx b/src/Components/CreateImageWizard/steps/Oscap/Oscap.tsx index 911aec2d..763349f1 100644 --- a/src/Components/CreateImageWizard/steps/Oscap/Oscap.tsx +++ b/src/Components/CreateImageWizard/steps/Oscap/Oscap.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { Alert, @@ -18,6 +18,7 @@ import { v4 as uuidv4 } from 'uuid'; import OscapProfileInformation from './OscapProfileInformation'; +import { usePoliciesQuery, PolicyRead } from '../../../../store/complianceApi'; import { useAppDispatch, useAppSelector } from '../../../../store/hooks'; import { DistributionProfileItem, @@ -29,9 +30,11 @@ import { Services, } from '../../../../store/imageBuilderApi'; import { - changeOscapProfile, + changeCompliance, selectDistribution, - selectProfile, + selectComplianceProfileID, + selectCompliancePolicyID, + selectCompliancePolicyTitle, addPackage, addPartition, changeFileSystemConfigurationType, @@ -42,47 +45,125 @@ import { changeMaskedServices, changeDisabledServices, changeKernelAppend, + selectComplianceType, } from '../../../../store/wizardSlice'; import { useHasSpecificTargetOnly } from '../../utilities/hasSpecificTargetOnly'; import { parseSizeUnit } from '../../utilities/parseSizeUnit'; import { Partition, Units } from '../FileSystem/FileSystemConfiguration'; +const OpenSCAPFGLabel = () => { + return ( + <> + OpenSCAP profile + + + To run a manual compliance scan in OpenSCAP, download this image. + + + } + > + + + + ); +}; + const ProfileSelector = () => { - const oscapProfile = useAppSelector(selectProfile); + const policyID = useAppSelector(selectCompliancePolicyID); + const policyTitle = useAppSelector(selectCompliancePolicyTitle); + const profileID = useAppSelector(selectComplianceProfileID); const release = useAppSelector(selectDistribution); + const majorVersion = release.split('-')[1]; const hasWslTargetOnly = useHasSpecificTargetOnly('wsl'); const dispatch = useAppDispatch(); const [isOpen, setIsOpen] = useState(false); + const complianceType = useAppSelector(selectComplianceType); + const { data: profiles, isFetching, isSuccess, isError, refetch, - } = useGetOscapProfilesQuery({ - distribution: release, - }); + } = useGetOscapProfilesQuery( + { + distribution: release, + }, + { + skip: complianceType === 'compliance', + } + ); + + const { + data: policies, + isFetching: isFetchingPolicies, + isSuccess: isSuccessPolicies, + } = usePoliciesQuery( + { + filter: `os_major_version=${majorVersion}`, + }, + { + skip: complianceType === 'openscap', + } + ); const { data: currentProfileData } = useGetOscapCustomizationsQuery( { distribution: release, // @ts-ignore if openScapProfile is undefined the query is going to get skipped - profile: oscapProfile, + profile: profileID, }, - { skip: !oscapProfile } + { skip: !profileID } ); const [trigger] = useLazyGetOscapCustomizationsQuery(); + useEffect(() => { + if (!policies || policies.data === undefined) { + return; + } + + if (policyID && !policyTitle) { + for (const p of policies.data) { + const pol = p as PolicyRead; + if (pol.id === policyID) { + dispatch( + changeCompliance({ + policyID: pol.id, + profileID: pol.ref_id, + policyTitle: pol.title, + }) + ); + } + } + } + }, [isSuccessPolicies]); + const handleToggle = () => { - if (!isOpen) { + if (!isOpen && complianceType === 'openscap') { refetch(); } setIsOpen(!isOpen); }; const handleClear = () => { - dispatch(changeOscapProfile(undefined)); + dispatch( + changeCompliance({ + profileID: undefined, + policyID: undefined, + policyTitle: undefined, + }) + ); clearOscapPackages(currentProfileData?.packages || []); dispatch(changeFileSystemConfigurationType('automatic')); handleServices(undefined); @@ -142,9 +223,9 @@ const ProfileSelector = () => { const handleSelect = ( _event: React.MouseEvent, - selection: OScapSelectOptionValueType + selection: OScapSelectOptionValueType | ComplianceSelectOptionValueType ) => { - if (selection.id === undefined) { + if (selection.profileID === undefined) { // handle user has selected 'None' case handleClear(); } else { @@ -152,7 +233,7 @@ const ProfileSelector = () => { trigger( { distribution: release, - profile: selection.id, + profile: selection.profileID as DistributionProfileItem, }, true // preferCacheValue ) @@ -164,7 +245,24 @@ const ProfileSelector = () => { handlePackages(oldOscapPackages, newOscapPackages); handleServices(response.services); dispatch(changeKernelAppend(response.kernel?.append || '')); - dispatch(changeOscapProfile(selection.id)); + if (complianceType === 'openscap') { + dispatch( + changeCompliance({ + profileID: selection.profileID, + policyID: undefined, + policyTitle: undefined, + }) + ); + } else { + const compl = selection as ComplianceSelectOptionValueType; + dispatch( + changeCompliance({ + profileID: compl.profileID, + policyID: compl.policyID, + policyTitle: compl.toString(), + }) + ); + } }); } setIsOpen(false); @@ -180,67 +278,77 @@ const ProfileSelector = () => { } }; + const complianceOptions = () => { + if (!policies || policies.data === undefined) { + return []; + } + + const res = []; + for (const p of policies.data) { + if (p === undefined) { + continue; + } + const pol = p as PolicyRead; + res.push(); + } + return res; + }; + return ( - OpenSCAP profile - - - To run a manual compliance scan in OpenSCAP, download this - image. - - - } - > - - - - } + label={complianceType === 'openscap' ? : <>Policy} > - { + if (profiles) { + return [].concat( + profiles.map((profile_id, index) => { + return ( + + ); + }) + ); + } + }} + > + {options()} + + )} + {complianceType === 'compliance' && ( + + ouiaId="compliancePolicySelect" + > + {complianceOptions()} + + )} {isError && ( string; }; @@ -291,7 +399,7 @@ const OScapSelectOption = ({ id: DistributionProfileItem, name?: string ): OScapSelectOptionValueType => ({ - id, + profileID: id, toString: () => name || '', }); @@ -303,9 +411,52 @@ const OScapSelectOption = ({ /> ); }; +type ComplianceSelectOptionPropType = { + policy: PolicyRead; +}; + +type ComplianceSelectOptionValueType = { + policyID: string; + profileID: string; + toString: () => string; +}; + +const ComplianceNoneOption = () => { + return ( + 'None', compareTo: () => false }} /> + ); +}; + +const ComplianceSelectOption = ({ policy }: ComplianceSelectOptionPropType) => { + const selectObj = ( + policyID: string, + profileID: string, + title: string + ): ComplianceSelectOptionValueType => ({ + policyID, + profileID, + toString: () => title, + }); + + const descr = ( + <> + Threshold: {policy.compliance_threshold} +
+ Active systems: {policy.total_system_count} + + ); + + return ( + + ); +}; export const Oscap = () => { - const oscapProfile = useAppSelector(selectProfile); + const oscapProfile = useAppSelector(selectComplianceProfileID); const environments = useAppSelector(selectImageTypes); return ( diff --git a/src/Components/CreateImageWizard/steps/Oscap/index.tsx b/src/Components/CreateImageWizard/steps/Oscap/index.tsx index 68279558..3516b03b 100644 --- a/src/Components/CreateImageWizard/steps/Oscap/index.tsx +++ b/src/Components/CreateImageWizard/steps/Oscap/index.tsx @@ -1,21 +1,44 @@ import React, { useEffect } from 'react'; -import { Button, Form, Text, Title } from '@patternfly/react-core'; +import { + Button, + Form, + FormGroup, + Radio, + Text, + Title, +} from '@patternfly/react-core'; import { ExternalLinkAltIcon } from '@patternfly/react-icons'; +import { useFlag } from '@unleash/proxy-client-react'; import { Oscap } from './Oscap'; -import { COMPLIANCE_AND_VULN_SCANNING_URL } from '../../../../constants'; +import { + COMPLIANCE_AND_VULN_SCANNING_URL, + COMPLIANCE_PROD_URL, + COMPLIANCE_STAGE_URL, +} from '../../../../constants'; import { imageBuilderApi } from '../../../../store/enhancedImageBuilderApi'; -import { useAppSelector } from '../../../../store/hooks'; -import { selectDistribution } from '../../../../store/wizardSlice'; +import { useAppDispatch, useAppSelector } from '../../../../store/hooks'; +import { + ComplianceType, + changeComplianceType, + selectDistribution, + selectComplianceType, +} from '../../../../store/wizardSlice'; +import { useGetEnvironment } from '../../../../Utilities/useGetEnvironment'; const OscapStep = () => { + const dispatch = useAppDispatch(); + const complianceEnabled = useFlag('image-builder.compliance.enabled'); + const complianceType = useAppSelector(selectComplianceType); const prefetchOscapProfile = imageBuilderApi.usePrefetch( 'getOscapProfiles', {} ); + const { isProd } = useGetEnvironment(); const release = useAppSelector(selectDistribution); + useEffect(() => { prefetchOscapProfile({ distribution: release }); // This useEffect hook should run *only* on mount and therefore has an empty @@ -26,24 +49,78 @@ const OscapStep = () => { return (
- OpenSCAP profile + {complianceEnabled ? 'Compliance' : 'OpenSCAP profile'} - - OpenSCAP enables you to automatically monitor the adherence of your - registered RHEL systems to a selected regulatory compliance profile. -
- -
+ {complianceEnabled && ( + + + dispatch(changeComplianceType('openscap' as ComplianceType)) + } + /> + + dispatch(changeComplianceType('compliance' as ComplianceType)) + } + /> + + )} + {(!complianceEnabled || complianceType === 'openscap') && ( + + OpenSCAP enables you to automatically monitor the adherence of your + registered RHEL systems to a selected regulatory compliance profile. +
+ +
+ )} + {complianceType === 'compliance' && ( + + Insights compliance enables you to monitor the adherence of your + registered RHEL systems to a selected compliance policy. +
+ +
+ +
+ )} ); diff --git a/src/constants.ts b/src/constants.ts index 910c15b1..c1883042 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -24,6 +24,10 @@ export const RELEASE_LIFECYCLE_URL = 'https://access.redhat.com/support/policy/updates/errata'; export const AZURE_AUTH_URL = 'https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow'; +export const COMPLIANCE_PROD_URL = + 'https://console.redhat.com/insights/compliance/scappolicies'; +export const COMPLIANCE_STAGE_URL = + 'https://console.stage.redhat.com/insights/compliance/scappolicies'; export const ACTIVATION_KEYS_PROD_URL = 'https://console.redhat.com/insights/connector/activation-keys'; export const ACTIVATION_KEYS_STAGE_URL =