Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,53 +7,71 @@ import { selectors } from '../../integration/vulnerabilities/workloadCves/Worklo
import { selectors as vulnerabilitiesSelectors } from '../../integration/vulnerabilities/vulnerabilities.selectors';
import pf6 from '../../selectors/pf6';

function visitFirstCve() {
withOcpAuth();
visitFromConsoleLeftNavExpandable('Security', 'Vulnerabilities');

return cy
.get(`${selectors.firstTableRow} td[data-label="CVE"]`)
.click()
.invoke('text')
.then((cveName) => {
cy.get('h1').contains(new RegExp(`^${cveName}$`));
return Promise.resolve(cveName);
});
}

describe('Security vulnerabilities - CVE Detail page', () => {
it('should navigate to the CVE Detail page and account for the project filter', () => {
withOcpAuth();
visitFromConsoleLeftNavExpandable('Security', 'Vulnerabilities');
visitFirstCve().then(() => {
// Verify that "All projects" is selected
cy.get(`.co-namespace-bar ${pf6.menuToggle}`).contains('All Projects');

// Visit a CVE page via link in the CVE table
cy.get(`${selectors.firstTableRow} td[data-label="CVE"]`)
.click()
.invoke('text')
.then((cveName) => {
cy.get('h1').contains(new RegExp(`^${cveName}$`));
// Click the deployment entity toggle
cy.get(vulnerabilitiesSelectors.entityTypeToggleItem('Deployment')).click();

// Verify that "All projects" is selected
cy.get(`.co-namespace-bar ${pf6.menuToggle}`).contains('All Projects');
// Columns that are always present in the table
const baseColumns = [
'Row expansion',
'Deployment',
'Images by severity',
'Images',
'First discovered',
];

// Click the deployment entity toggle
cy.get(vulnerabilitiesSelectors.entityTypeToggleItem('Deployment')).click();
const topLevelTableSelector = 'table:first-of-type';

// Columns that are always present in the table
const baseColumns = [
'Row expansion',
'Deployment',
'Images by severity',
'Images',
'First discovered',
];
// Verify that the "Namespace" column is present
assertVisibleTableColumns(topLevelTableSelector, [...baseColumns, 'Namespace']);

const topLevelTableSelector = 'table:first-of-type';
// Verify that Namespace is present in the search entities
assertSearchEntities(['Image', 'Image component', 'Deployment', 'Namespace']);

// Verify that the "Namespace" column is present
assertVisibleTableColumns(topLevelTableSelector, [...baseColumns, 'Namespace']);
// Change to the 'stackrox' project
selectProject('stackrox');

// Verify that Namespace is present in the search entities
assertSearchEntities(['Image', 'Image component', 'Deployment', 'Namespace']);
// Wait for the table data to update
cy.get(selectors.loadingSpinner).should('exist');
cy.get(selectors.loadingSpinner).should('not.exist');

// Change to the 'stackrox' project
selectProject('stackrox');
// Verify that the "Namespace" column is not present
assertVisibleTableColumns(topLevelTableSelector, [...baseColumns]);

// Wait for the table data to update
cy.get(selectors.loadingSpinner).should('exist');
cy.get(selectors.loadingSpinner).should('not.exist');
// Verify that Namespace is not present in the search entities
assertSearchEntities(['Image', 'Image component', 'Deployment']);
});
});

// Verify that the "Namespace" column is not present
assertVisibleTableColumns(topLevelTableSelector, [...baseColumns]);
it('should navigate to an affected image detail page', () => {
visitFirstCve().then(() => {
cy.get(vulnerabilitiesSelectors.entityTypeToggleItem('Image')).click();

// Verify that Namespace is not present in the search entities
assertSearchEntities(['Image', 'Image component', 'Deployment']);
});
cy.get(`${selectors.firstTableRow} td[data-label="Image"] a`)
.click()
.then(([$imageLink]) => {
const imageName = $imageLink.innerText.replace('\n', '');
cy.get('h1').contains(imageName);
});
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { visitFromConsoleLeftNavExpandable } from '../../helpers/nav';
import { withOcpAuth } from '../../helpers/ocpAuth';
import { assertVisibleTableColumns } from '../../helpers/tableHelpers';
import { selectors } from '../../integration/vulnerabilities/workloadCves/WorkloadCves.selectors';
import { selectors as vulnerabilitiesSelectors } from '../../integration/vulnerabilities/vulnerabilities.selectors';
import { selectProject } from '../../helpers/ocpConsole';

function visitImageDetailPage() {
withOcpAuth();
visitFromConsoleLeftNavExpandable('Security', 'Vulnerabilities');

cy.get(vulnerabilitiesSelectors.entityTypeToggleItem('Image')).click();

// Visit an image page via link in the image table
return cy
.get(`${selectors.firstTableRow} td[data-label="Image"] a`)
.click()
.then(([$imageLink]) => {
const imageName = $imageLink.innerText.replace('\n', '');
cy.get('h1').contains(imageName);
return Promise.resolve(imageName);
});
}

describe('Security vulnerabilities - Image Detail page', () => {
it('should show the appropriate table columns on the workload resources tab', () => {
visitImageDetailPage()
.then(() => {
cy.get('button[role="tab"]:contains("Resources")').click();

// By default, the project filter should be "All projects" which will show the Namespace column
const expectedColumns = ['Name', 'Namespace', 'Created'];
assertVisibleTableColumns('table', expectedColumns);

// The user could also navigate to this page when viewing a project that has a workload containing the image.
// Grab the namespace of a known workload so we can select that project.
return cy
.get(`${selectors.firstTableRow} td[data-label="Namespace"]`)
.then(([$ns]) => Promise.resolve($ns.innerText));
})
.then((namespace) => {
// Select the project that has the workload containing the image and verify the columns
selectProject(namespace);

const expectedColumns = ['Name', 'Created'];
assertVisibleTableColumns('table', expectedColumns);
});
});

it('should navigate to the CVE Detail from the vulnerability table for the image', () => {
visitImageDetailPage()
.then(() => {
return cy
.get(`${selectors.firstTableRow} td[data-label="CVE"] a`)
.click()
.then(([$cveLink]) => Promise.resolve($cveLink.innerText.replace('\n', '')));
})
.then((cveName) => {
cy.get('h1').contains(cveName);
});
});
});
Original file line number Diff line number Diff line change
@@ -1,16 +1,32 @@
import React from 'react';
import { useActiveNamespace } from '@openshift-console/dynamic-plugin-sdk';
import { useParams } from 'react-router-dom-v5-compat';
import { NamespaceBar, useActiveNamespace } from '@openshift-console/dynamic-plugin-sdk';

import { ALL_NAMESPACES_KEY } from 'ConsolePlugin/constants';
import useURLSearch from 'hooks/useURLSearch';
import ImagePage from 'Containers/Vulnerabilities/WorkloadCves/Image/ImagePage';
import { useDefaultWorkloadCveViewContext } from 'ConsolePlugin/hooks/useDefaultWorkloadCveViewContext';
import { WorkloadCveViewContext } from 'Containers/Vulnerabilities/WorkloadCves/WorkloadCveViewContext';
import { hideColumnIf } from 'hooks/useManagedColumns';

export function ImageDetailPage() {
const [namespace] = useActiveNamespace();
const { imageId } = useParams();
const { searchFilter, setSearchFilter } = useURLSearch();
const context = useDefaultWorkloadCveViewContext();
const [activeNamespace] = useActiveNamespace();

return (
<>
<div>Image Detail Page</div>
<div>Namespace: {namespace}</div>
<div>Image ID: {imageId}</div>
</>
<WorkloadCveViewContext.Provider value={context}>
<NamespaceBar
// Force clear Namespace filter when the user changes the namespace via the NamespaceBar
onNamespaceChange={() => setSearchFilter({ ...searchFilter, Namespace: [] })}
/>
<ImagePage
showVulnerabilityStateTabs={false}
vulnerabilityState="OBSERVED"
deploymentResourceColumnOverrides={{
cluster: hideColumnIf(true),
namespace: hideColumnIf(activeNamespace !== ALL_NAMESPACES_KEY),
}}
/>
</WorkloadCveViewContext.Provider>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,34 @@ import { Table, Thead, Tr, Th, Tbody, Td } from '@patternfly/react-table';
import { gql } from '@apollo/client';

import { UseURLSortResult } from 'hooks/useURLSort';
import { generateVisibilityForColumns, ManagedColumns } from 'hooks/useManagedColumns';
import DateDistance from 'Components/DateDistance';
import EmptyTableResults from '../components/EmptyTableResults';
import useVulnerabilityState from '../hooks/useVulnerabilityState';
import useWorkloadCveViewContext from '../hooks/useWorkloadCveViewContext';

export const deploymentResourcesTableId = 'DeploymentResourcesTable';

export const defaultColumns = {
name: {
title: 'Name',
isShownByDefault: true,
isUntoggleAble: true,
},
cluster: {
title: 'Cluster',
isShownByDefault: true,
},
namespace: {
title: 'Namespace',
isShownByDefault: true,
},
created: {
title: 'Created',
isShownByDefault: true,
},
} as const;

export type DeploymentResources = {
deploymentCount: number;
deployments: {
Expand Down Expand Up @@ -38,19 +61,34 @@ export const deploymentResourcesFragment = gql`
export type DeploymentResourceTableProps = {
data: DeploymentResources;
getSortParams: UseURLSortResult['getSortParams'];
columnVisibilityState: ManagedColumns<keyof typeof defaultColumns>['columns'];
};

function DeploymentResourceTable({ data, getSortParams }: DeploymentResourceTableProps) {
function DeploymentResourceTable({
data,
getSortParams,
columnVisibilityState,
}: DeploymentResourceTableProps) {
const { urlBuilder } = useWorkloadCveViewContext();
const vulnerabilityState = useVulnerabilityState();
const getVisibilityClass = generateVisibilityForColumns(columnVisibilityState);
return (
<Table borders={false} variant="compact">
<Thead noWrap>
<Tr>
<Th sort={getSortParams('Deployment')}>Name</Th>
<Th sort={getSortParams('Cluster')}>Cluster</Th>
<Th sort={getSortParams('Namespace')}>Namespace</Th>
<Th>Created</Th>
<Th className={getVisibilityClass('name')} sort={getSortParams('Deployment')}>
Name
</Th>
<Th className={getVisibilityClass('cluster')} sort={getSortParams('Cluster')}>
Cluster
</Th>
<Th
className={getVisibilityClass('namespace')}
sort={getSortParams('Namespace')}
>
Namespace
</Th>
<Th className={getVisibilityClass('created')}>Created</Th>
</Tr>
</Thead>
{data.deployments.length === 0 && <EmptyTableResults colSpan={4} />}
Expand All @@ -63,7 +101,7 @@ function DeploymentResourceTable({ data, getSortParams }: DeploymentResourceTabl
}}
>
<Tr>
<Td dataLabel="Name">
<Td dataLabel="Name" className={getVisibilityClass('name')}>
<Link
to={urlBuilder.workloadDetails(
{ id, namespace, name, type },
Expand All @@ -73,9 +111,13 @@ function DeploymentResourceTable({ data, getSortParams }: DeploymentResourceTabl
{name}
</Link>
</Td>
<Td dataLabel="Cluster">{clusterName}</Td>
<Td dataLabel="Namespace">{namespace}</Td>
<Td dataLabel="Created">
<Td dataLabel="Cluster" className={getVisibilityClass('cluster')}>
{clusterName}
</Td>
<Td dataLabel="Namespace" className={getVisibilityClass('namespace')}>
{namespace}
</Td>
<Td dataLabel="Created" className={getVisibilityClass('created')}>
<DateDistance date={created} />
</Td>
</Tr>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { getAxiosErrorMessage } from 'utils/responseErrorUtils';
import useIsScannerV4Enabled from 'hooks/useIsScannerV4Enabled';
import usePermissions from 'hooks/usePermissions';
import useURLPagination from 'hooks/useURLPagination';
import type { ColumnConfigOverrides } from 'hooks/useManagedColumns';
import type { VulnerabilityState } from 'types/cve.proto';

import HeaderLoadingSkeleton from '../../components/HeaderLoadingSkeleton';
Expand All @@ -49,6 +50,7 @@ import getImageScanMessage from '../utils/getImageScanMessage';
import { DEFAULT_VM_PAGE_SIZE } from '../../constants';
import { getImageBaseNameDisplay } from '../utils/images';
import useWorkloadCveViewContext from '../hooks/useWorkloadCveViewContext';
import { defaultColumns as deploymentResourcesDefaultColumns } from './DeploymentResourceTable';

export const imageDetailsQuery = gql`
${imageDetailsFragment}
Expand Down Expand Up @@ -82,9 +84,16 @@ function OptionalSbomButtonTooltip({
export type ImagePageProps = {
vulnerabilityState: VulnerabilityState;
showVulnerabilityStateTabs: boolean;
deploymentResourceColumnOverrides: ColumnConfigOverrides<
keyof typeof deploymentResourcesDefaultColumns
>;
};

function ImagePage({ vulnerabilityState, showVulnerabilityStateTabs }: ImagePageProps) {
function ImagePage({
vulnerabilityState,
showVulnerabilityStateTabs,
deploymentResourceColumnOverrides,
}: ImagePageProps) {
const { urlBuilder, pageTitle } = useWorkloadCveViewContext();
const { imageId } = useParams() as { imageId: string };
const { data, error } = useQuery<
Expand Down Expand Up @@ -262,7 +271,13 @@ function ImagePage({ vulnerabilityState, showVulnerabilityStateTabs }: ImagePage
eventKey="Resources"
title={<TabTitleText>Resources</TabTitleText>}
>
<ImagePageResources imageId={imageId} pagination={pagination} />
<ImagePageResources
imageId={imageId}
pagination={pagination}
deploymentResourceColumnOverrides={
deploymentResourceColumnOverrides
}
/>
</Tab>
<Tab
className="pf-v5-u-display-flex pf-v5-u-flex-direction-column pf-v5-u-flex-grow-1"
Expand Down
Loading
Loading