foreman_openscap 4.3.0 → 4.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/lib/foreman_openscap/version.rb +1 -1
  3. data/package.json +48 -0
  4. data/webpack/components/EmptyState.js +67 -0
  5. data/webpack/components/IndexLayout.js +35 -0
  6. data/webpack/components/IndexLayout.scss +3 -0
  7. data/webpack/components/IndexTable/IndexTableHelper.js +9 -0
  8. data/webpack/components/IndexTable/index.js +66 -0
  9. data/webpack/components/RuleSeverity/RuleSeverity.scss +3 -0
  10. data/webpack/components/RuleSeverity/RuleSeverity.test.js +13 -0
  11. data/webpack/components/RuleSeverity/__snapshots__/RuleSeverity.test.js.snap +41 -0
  12. data/webpack/components/RuleSeverity/i_severity-critical.svg +61 -0
  13. data/webpack/components/RuleSeverity/i_severity-high.svg +61 -0
  14. data/webpack/components/RuleSeverity/i_severity-low.svg +62 -0
  15. data/webpack/components/RuleSeverity/i_severity-med.svg +62 -0
  16. data/webpack/components/RuleSeverity/i_unknown.svg +33 -0
  17. data/webpack/components/RuleSeverity/index.js +33 -0
  18. data/webpack/components/withLoading.js +68 -0
  19. data/webpack/global_index.js +5 -0
  20. data/webpack/graphql/queries/cves.gql +18 -0
  21. data/webpack/graphql/queries/ovalContents.gql +11 -0
  22. data/webpack/graphql/queries/ovalPolicies.gql +12 -0
  23. data/webpack/graphql/queries/ovalPolicy.gql +21 -0
  24. data/webpack/helpers/commonHelper.js +1 -0
  25. data/webpack/helpers/globalIdHelper.js +13 -0
  26. data/webpack/helpers/pageParamsHelper.js +31 -0
  27. data/webpack/helpers/pathsHelper.js +22 -0
  28. data/webpack/helpers/tableHelper.js +9 -0
  29. data/webpack/index.js +8 -0
  30. data/webpack/routes/OvalContents/OvalContentsIndex/OvalContentsIndex.js +45 -0
  31. data/webpack/routes/OvalContents/OvalContentsIndex/OvalContentsTable.js +38 -0
  32. data/webpack/routes/OvalContents/OvalContentsIndex/__tests__/OvalContentsIndex.fixtures.js +106 -0
  33. data/webpack/routes/OvalContents/OvalContentsIndex/__tests__/OvalContentsIndex.test.js +75 -0
  34. data/webpack/routes/OvalContents/OvalContentsIndex/index.js +7 -0
  35. data/webpack/routes/OvalPolicies/OvalPoliciesIndex/OvalPoliciesIndex.js +46 -0
  36. data/webpack/routes/OvalPolicies/OvalPoliciesIndex/OvalPoliciesTable.js +44 -0
  37. data/webpack/routes/OvalPolicies/OvalPoliciesIndex/__tests__/OvalPoliciesIndex.fixtures.js +61 -0
  38. data/webpack/routes/OvalPolicies/OvalPoliciesIndex/__tests__/OvalPoliciesIndex.test.js +78 -0
  39. data/webpack/routes/OvalPolicies/OvalPoliciesIndex/index.js +7 -0
  40. data/webpack/routes/OvalPolicies/OvalPoliciesShow/CvesTab.js +48 -0
  41. data/webpack/routes/OvalPolicies/OvalPoliciesShow/CvesTable.js +63 -0
  42. data/webpack/routes/OvalPolicies/OvalPoliciesShow/OvalPoliciesShow.js +79 -0
  43. data/webpack/routes/OvalPolicies/OvalPoliciesShow/OvalPoliciesShowHelper.js +39 -0
  44. data/webpack/routes/OvalPolicies/OvalPoliciesShow/__tests__/OvalPoliciesShow.fixtures.js +78 -0
  45. data/webpack/routes/OvalPolicies/OvalPoliciesShow/__tests__/OvalPoliciesShow.test.js +112 -0
  46. data/webpack/routes/OvalPolicies/OvalPoliciesShow/index.js +35 -0
  47. data/webpack/routes/routes.js +28 -0
  48. data/webpack/testHelper.js +64 -0
  49. metadata +48 -2
@@ -0,0 +1,63 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { translate as __ } from 'foremanReact/common/I18n';
4
+
5
+ import { linkCell } from '../../../helpers/tableHelper';
6
+ import { hostsPath } from '../../../helpers/pathsHelper';
7
+ import { decodeId } from '../../../helpers/globalIdHelper';
8
+ import { addSearch } from '../../../helpers/pageParamsHelper';
9
+
10
+ import withLoading from '../../../components/withLoading';
11
+ import IndexTable from '../../../components/IndexTable';
12
+
13
+ const CvesTable = props => {
14
+ const columns = [
15
+ { title: __('Ref Id') },
16
+ { title: __('Has Errata?') },
17
+ { title: __('Hosts Count') },
18
+ ];
19
+
20
+ const cveRefId = cve => (
21
+ <a href={cve.refUrl} rel="noopener noreferrer" target="_blank">
22
+ {cve.refId}
23
+ </a>
24
+ );
25
+
26
+ const hostCount = cve =>
27
+ linkCell(
28
+ addSearch(hostsPath, { search: `cve_id = ${decodeId(cve)}` }),
29
+ cve.hosts.nodes.length
30
+ );
31
+
32
+ const rows = props.cves.map(cve => ({
33
+ cells: [
34
+ { title: cveRefId(cve) },
35
+ { title: cve.hasErrata ? __('Yes') : __('No') },
36
+ { title: hostCount(cve) },
37
+ ],
38
+ cve,
39
+ }));
40
+
41
+ const actions = [];
42
+
43
+ return (
44
+ <IndexTable
45
+ columns={columns}
46
+ rows={rows}
47
+ actions={actions}
48
+ pagination={props.pagination}
49
+ totalCount={props.totalCount}
50
+ history={props.history}
51
+ ariaTableLabel={__('Table of CVEs for OVAL policy')}
52
+ />
53
+ );
54
+ };
55
+
56
+ CvesTable.propTypes = {
57
+ cves: PropTypes.array.isRequired,
58
+ pagination: PropTypes.object.isRequired,
59
+ totalCount: PropTypes.number.isRequired,
60
+ history: PropTypes.object.isRequired,
61
+ };
62
+
63
+ export default withLoading(CvesTable);
@@ -0,0 +1,79 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { Link } from 'react-router-dom';
4
+ import { Helmet } from 'react-helmet';
5
+ import { translate as __ } from 'foremanReact/common/I18n';
6
+ import {
7
+ Button,
8
+ Grid,
9
+ GridItem,
10
+ TextContent,
11
+ Text,
12
+ TextVariants,
13
+ Tabs,
14
+ Tab,
15
+ TabTitleText,
16
+ } from '@patternfly/react-core';
17
+ import '@patternfly/patternfly/patternfly-addons.scss';
18
+
19
+ import withLoading from '../../../components/withLoading';
20
+
21
+ import CvesTab from './CvesTab';
22
+
23
+ import { policySchedule, newJobFormPath } from './OvalPoliciesShowHelper';
24
+ import { resolvePath } from '../../../helpers/pathsHelper';
25
+
26
+ const OvalPoliciesShow = props => {
27
+ const { policy, match, history } = props;
28
+ const activeTab = match.params.tab ? match.params.tab : 'details';
29
+
30
+ const handleTabSelect = (event, value) => {
31
+ history.push(
32
+ resolvePath(match.path, { ':id': match.params.id, ':tab?': value })
33
+ );
34
+ };
35
+
36
+ return (
37
+ <React.Fragment>
38
+ <Helmet>
39
+ <title>{`${policy.name} | OVAL Policy`}</title>
40
+ </Helmet>
41
+ <Grid className="scap-page-grid">
42
+ <GridItem span={10}>
43
+ <Text component={TextVariants.h1}>{policy.name}</Text>
44
+ </GridItem>
45
+ <GridItem span={2}>
46
+ <Link to={newJobFormPath(policy, match.params.id)}>
47
+ <Button variant="secondary">{__('Scan All Hostgroups')}</Button>
48
+ </Link>
49
+ </GridItem>
50
+ <GridItem span={12}>
51
+ <Tabs mountOnEnter activeKey={activeTab} onSelect={handleTabSelect}>
52
+ <Tab
53
+ eventKey="details"
54
+ title={<TabTitleText>Details</TabTitleText>}
55
+ >
56
+ <TextContent className="pf-u-pt-md">
57
+ <Text component={TextVariants.h3}>Period</Text>
58
+ <Text component={TextVariants.p}>{policySchedule(policy)}</Text>
59
+ <Text component={TextVariants.h3}>Description</Text>
60
+ <Text component={TextVariants.p}>{policy.description}</Text>
61
+ </TextContent>
62
+ </Tab>
63
+ <Tab eventKey="cves" title={<TabTitleText>CVEs</TabTitleText>}>
64
+ <CvesTab {...props} />
65
+ </Tab>
66
+ </Tabs>
67
+ </GridItem>
68
+ </Grid>
69
+ </React.Fragment>
70
+ );
71
+ };
72
+
73
+ OvalPoliciesShow.propTypes = {
74
+ match: PropTypes.object.isRequired,
75
+ history: PropTypes.object.isRequired,
76
+ policy: PropTypes.object.isRequired,
77
+ };
78
+
79
+ export default withLoading(OvalPoliciesShow);
@@ -0,0 +1,39 @@
1
+ import { decodeId } from '../../../helpers/globalIdHelper';
2
+ import { addSearch } from '../../../helpers/pageParamsHelper';
3
+ import { newJobPath } from '../../../helpers/pathsHelper';
4
+
5
+ export const policySchedule = policy => {
6
+ switch (policy.period) {
7
+ case 'weekly':
8
+ return `Weekly, on ${policy.weekday}`;
9
+ case 'monthly':
10
+ return `Monthly, day of month: ${policy.dayOfMonth}`;
11
+ case 'custom':
12
+ return `Custom cron: ${policy.cronLine}`;
13
+ default:
14
+ return 'Unknown schedule';
15
+ }
16
+ };
17
+
18
+ const targetingScopedSearchQuery = policy => {
19
+ const hgIds = policy.hostgroups.nodes.reduce((memo, hg) => {
20
+ const ids = [decodeId(hg)].concat(hg.descendants.nodes.map(decodeId));
21
+ return ids.reduce(
22
+ (acc, id) => (acc.includes(id) ? acc : [...acc, id]),
23
+ memo
24
+ );
25
+ }, []);
26
+
27
+ if (hgIds.length === 0) {
28
+ return '';
29
+ }
30
+
31
+ return `hostgroup_id ^ (${hgIds.join(' ')})`;
32
+ };
33
+
34
+ export const newJobFormPath = (policy, policyId) =>
35
+ addSearch(newJobPath, {
36
+ feature: 'foreman_openscap_run_oval_scans',
37
+ host_ids: targetingScopedSearchQuery(policy),
38
+ 'inputs[oval_policies]': policyId,
39
+ });
@@ -0,0 +1,78 @@
1
+ import { mockFactory } from '../../../../testHelper';
2
+ import ovalPolicyQuery from '../../../../graphql/queries/ovalPolicy.gql';
3
+ import cvesQuery from '../../../../graphql/queries/cves.gql';
4
+
5
+ const policyDetailMockFactory = mockFactory('ovalPolicy', ovalPolicyQuery);
6
+ const cvesMockFactory = mockFactory('cves', cvesQuery);
7
+
8
+ const ovalPolicy = {
9
+ id: 'MDE6Rm9yZW1hbk9wZW5zY2FwOjpPdmFsUG9saWN5LTM=',
10
+ name: 'Third policy',
11
+ period: 'weekly',
12
+ cronLine: null,
13
+ weekday: 'tuesday',
14
+ dayOfMonth: null,
15
+ description: 'A very strict policy',
16
+ hostgroups: {
17
+ nodes: [
18
+ {
19
+ id: 'MDE6SG9zdGdyb3VwLTQ=',
20
+ name: 'oval hg',
21
+ descendants: {
22
+ nodes: [
23
+ { id: 'MDE6SG9zdGdyb3VwLTEw' },
24
+ { id: 'MDE6SG9zdGdyb3VwLTEy' },
25
+ { id: 'MDE6SG9zdGdyb3VwLTEx' },
26
+ ],
27
+ },
28
+ },
29
+ ],
30
+ },
31
+ };
32
+
33
+ const cvesResult = {
34
+ totalCount: 1,
35
+ nodes: [
36
+ {
37
+ id: 'MDE6Rm9yZW1hbk9wZW5zY2FwOjpDdmUtMjY3',
38
+ refId: 'CVE-2020-14365',
39
+ refUrl: 'https://access.redhat.com/security/cve/CVE-2020-14365',
40
+ definitionId: 'oval:com.redhat.rhsa:def:20203601',
41
+ hasErrata: true,
42
+ hosts: {
43
+ nodes: [
44
+ {
45
+ id: 'MDE6SG9zdC0z',
46
+ name: 'centos-random.example.com',
47
+ },
48
+ ],
49
+ },
50
+ },
51
+ ],
52
+ };
53
+
54
+ export const ovalPolicyId = 3;
55
+
56
+ export const pushMock = jest.fn();
57
+
58
+ export const historyMock = {
59
+ location: {
60
+ search: '',
61
+ },
62
+ push: pushMock,
63
+ };
64
+
65
+ export const historyWithSearch = {
66
+ location: {
67
+ search: '?page=1&perPage=5',
68
+ },
69
+ };
70
+
71
+ export const policyDetailMock = policyDetailMockFactory(
72
+ { id: ovalPolicy.id },
73
+ ovalPolicy
74
+ );
75
+ export const policyCvesMock = cvesMockFactory(
76
+ { search: `oval_policy_id = ${ovalPolicyId}`, first: 5, last: 5 },
77
+ cvesResult
78
+ );
@@ -0,0 +1,112 @@
1
+ import React from 'react';
2
+ import { Router } from 'react-router-dom';
3
+ import { render, screen, waitFor } from '@testing-library/react';
4
+ import { within } from '@testing-library/dom';
5
+ import '@testing-library/jest-dom';
6
+ import userEvent from '@testing-library/user-event';
7
+ import { createMemoryHistory } from 'history';
8
+
9
+ import OvalPoliciesShow from '../index';
10
+ import {
11
+ ovalPoliciesShowPath,
12
+ resolvePath,
13
+ } from '../../../../helpers/pathsHelper';
14
+
15
+ import { withMockedProvider, tick, withRouter } from '../../../../testHelper';
16
+ import {
17
+ policyDetailMock,
18
+ historyMock,
19
+ historyWithSearch,
20
+ pushMock,
21
+ policyCvesMock,
22
+ ovalPolicyId,
23
+ } from './OvalPoliciesShow.fixtures';
24
+
25
+ const TestComponent = withRouter(withMockedProvider(OvalPoliciesShow));
26
+
27
+ describe('OvalPoliciesShow', () => {
28
+ it('should load details by default and handle tab change', async () => {
29
+ const { container } = render(
30
+ <TestComponent
31
+ history={historyMock}
32
+ match={{ params: { id: ovalPolicyId }, path: ovalPoliciesShowPath }}
33
+ mocks={policyDetailMock}
34
+ />
35
+ );
36
+ expect(screen.getByText('Loading')).toBeInTheDocument();
37
+ await waitFor(tick);
38
+ expect(screen.queryByText('Loading')).not.toBeInTheDocument();
39
+ expect(screen.getByText('Third policy')).toBeInTheDocument();
40
+ expect(screen.getByText('Weekly, on tuesday')).toBeInTheDocument();
41
+ expect(screen.getByText('A very strict policy')).toBeInTheDocument();
42
+ const activeTabHeader = container.querySelector(
43
+ '.pf-c-tabs__item.pf-m-current'
44
+ );
45
+ expect(within(activeTabHeader).getByText('Details')).toBeInTheDocument();
46
+ userEvent.click(screen.getByRole('button', { name: 'CVEs' }));
47
+ expect(pushMock).toHaveBeenCalledWith(
48
+ resolvePath(ovalPoliciesShowPath, {
49
+ ':id': ovalPolicyId,
50
+ ':tab?': 'cves',
51
+ })
52
+ );
53
+ });
54
+ it('should load details tab when specified in URL', async () => {
55
+ render(
56
+ <TestComponent
57
+ history={historyMock}
58
+ match={{
59
+ params: { id: ovalPolicyId, tab: 'details' },
60
+ path: ovalPoliciesShowPath,
61
+ }}
62
+ mocks={policyDetailMock}
63
+ />
64
+ );
65
+ expect(screen.getByText('Loading')).toBeInTheDocument();
66
+ await waitFor(tick);
67
+ expect(screen.queryByText('Loading')).not.toBeInTheDocument();
68
+ expect(screen.getByText('Weekly, on tuesday')).toBeInTheDocument();
69
+ });
70
+ it('should load CVEs tab when specified in URL', async () => {
71
+ const mocks = policyDetailMock.concat(policyCvesMock);
72
+ render(
73
+ <TestComponent
74
+ history={historyWithSearch}
75
+ match={{
76
+ params: { id: ovalPolicyId, tab: 'cves' },
77
+ path: ovalPoliciesShowPath,
78
+ }}
79
+ mocks={mocks}
80
+ />
81
+ );
82
+ expect(screen.getByText('Loading')).toBeInTheDocument();
83
+ await waitFor(tick);
84
+ await waitFor(tick);
85
+ expect(screen.queryByText('Loading')).not.toBeInTheDocument();
86
+ expect(screen.getByText('CVE-2020-14365')).toBeInTheDocument();
87
+ });
88
+ it('should have button for scanning all hostgroups', async () => {
89
+ const btnText = 'Scan All Hostgroups';
90
+
91
+ const WithProvider = withMockedProvider(OvalPoliciesShow);
92
+ const history = createMemoryHistory();
93
+ history.push = jest.fn();
94
+
95
+ render(
96
+ <Router history={history}>
97
+ <WithProvider
98
+ history={history}
99
+ match={{ params: { id: ovalPolicyId }, path: ovalPoliciesShowPath }}
100
+ mocks={policyDetailMock}
101
+ />
102
+ </Router>
103
+ );
104
+ await waitFor(tick);
105
+ expect(screen.queryByText('Loading')).not.toBeInTheDocument();
106
+ expect(screen.getByText(btnText)).toBeInTheDocument();
107
+ userEvent.click(screen.getByRole('button', { name: btnText }));
108
+ expect(history.push).toHaveBeenCalledWith(
109
+ '/job_invocations/new?feature=foreman_openscap_run_oval_scans&host_ids=hostgroup_id+%5E+%284+10+12+11%29&inputs%5Boval_policies%5D=3'
110
+ );
111
+ });
112
+ });
@@ -0,0 +1,35 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { useQuery } from '@apollo/client';
4
+
5
+ import { translate as __ } from 'foremanReact/common/I18n';
6
+
7
+ import OvalPoliciesShow from './OvalPoliciesShow';
8
+ import { encodeId } from '../../../helpers/globalIdHelper';
9
+
10
+ import ovalPolicy from '../../../graphql/queries/ovalPolicy.gql';
11
+
12
+ const WrappedOvalPoliciesShow = props => {
13
+ const id = encodeId('ForemanOpenscap::OvalPolicy', props.match.params.id);
14
+
15
+ const useFetchFn = componentProps =>
16
+ useQuery(ovalPolicy, { variables: { id } });
17
+
18
+ const renameData = data => ({ policy: data.ovalPolicy });
19
+
20
+ return (
21
+ <OvalPoliciesShow
22
+ {...props}
23
+ fetchFn={useFetchFn}
24
+ renameData={renameData}
25
+ resultPath="ovalPolicy"
26
+ emptyStateTitle={__('No OVAL Policy found')}
27
+ />
28
+ );
29
+ };
30
+
31
+ WrappedOvalPoliciesShow.propTypes = {
32
+ match: PropTypes.object.isRequired,
33
+ };
34
+
35
+ export default WrappedOvalPoliciesShow;
@@ -0,0 +1,28 @@
1
+ import React from 'react';
2
+ import OvalContentsIndex from './OvalContents/OvalContentsIndex';
3
+ import OvalPoliciesIndex from './OvalPolicies/OvalPoliciesIndex';
4
+ import OvalPoliciesShow from './OvalPolicies/OvalPoliciesShow';
5
+
6
+ import {
7
+ ovalContentsPath,
8
+ ovalPoliciesPath,
9
+ ovalPoliciesShowPath,
10
+ } from '../helpers/pathsHelper';
11
+
12
+ export default [
13
+ {
14
+ path: ovalContentsPath,
15
+ render: props => <OvalContentsIndex {...props} />,
16
+ exact: true,
17
+ },
18
+ {
19
+ path: ovalPoliciesPath,
20
+ render: props => <OvalPoliciesIndex {...props} />,
21
+ exact: true,
22
+ },
23
+ {
24
+ path: ovalPoliciesShowPath,
25
+ render: props => <OvalPoliciesShow {...props} />,
26
+ exact: true,
27
+ },
28
+ ];
@@ -0,0 +1,64 @@
1
+ import React from 'react';
2
+ import { MockedProvider } from '@apollo/react-testing';
3
+ import { MemoryRouter } from 'react-router-dom';
4
+ import { getForemanContext } from 'foremanReact/Root/Context/ForemanContext';
5
+
6
+ export const withRouter = Component => props => (
7
+ <MemoryRouter>
8
+ <Component {...props} />
9
+ </MemoryRouter>
10
+ );
11
+
12
+ export const withMockedProvider = Component => props => {
13
+ const ForemanContext = getForemanContext(ctx);
14
+ // eslint-disable-next-line react/prop-types
15
+ const { mocks, ...rest } = props;
16
+
17
+ const ctx = {
18
+ metadata: {
19
+ UISettings: {
20
+ perPage: 20,
21
+ },
22
+ },
23
+ };
24
+
25
+ return (
26
+ <ForemanContext.Provider value={ctx}>
27
+ <MockedProvider mocks={mocks} addTypename={false}>
28
+ <Component {...rest} />
29
+ </MockedProvider>
30
+ </ForemanContext.Provider>
31
+ );
32
+ };
33
+
34
+ // use to resolve async mock requests for apollo MockedProvider
35
+ export const tick = () => new Promise(resolve => setTimeout(resolve, 0));
36
+
37
+ export const historyMock = {
38
+ location: {
39
+ search: '',
40
+ },
41
+ };
42
+
43
+ export const mockFactory = (resultName, query) => (
44
+ variables,
45
+ modelResults,
46
+ errors = []
47
+ ) => {
48
+ const mock = {
49
+ request: {
50
+ query,
51
+ variables,
52
+ },
53
+ result: {
54
+ data: {
55
+ [resultName]: modelResults,
56
+ },
57
+ },
58
+ };
59
+
60
+ if (errors.length !== 0) {
61
+ mock.result.errors = errors;
62
+ }
63
+ return [mock];
64
+ };