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 @@ -8,22 +8,46 @@
"registry": "docker.io",
"remote": "cypress-test/image",
"tag": "v0.0.1",
"fullName": "docker.io/cypress-test/image:v0.0.1"
"fullName": "docker.io/cypress-test/image:v0.0.1",
"__typename": "ImageName"
},
"imageCVECountBySeverity": {
"critical": { "total": 2 },
"important": { "total": 0 },
"moderate": { "total": 0 },
"low": { "total": 0 },
"unknown": { "total": 0 }
"critical": {
"total": 0,
"__typename": "ResourceCountByFixability"
},
"important": {
"total": 2,
"__typename": "ResourceCountByFixability"
},
"moderate": {
"total": 0,
"__typename": "ResourceCountByFixability"
},
"low": {
"total": 0,
"__typename": "ResourceCountByFixability"
},
"unknown": {
"total": 0,
"__typename": "ResourceCountByFixability"
},
"__typename": "ResourceCountByCVESeverity"
},
"operatingSystem": "debian:11",
"operatingSystem": "debian:8",
"deploymentCount": 1,
"watchStatus": "NOT_WATCHED",
"metadata": { "v1": { "created": "2024-01-15T10:30:00Z" } },
"scanTime": "2024-06-01T12:00:00Z",
"metadata": {
"v1": {
"created": "2020-09-15T22:35:00.560827473Z",
"__typename": "V1Metadata"
},
"__typename": "ImageMetadata"
},
"scanTime": "2024-12-02T13:51:19.876935617Z",
"scanNotes": [],
"notes": []
"notes": [],
"__typename": "Image"
}
]
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"data": {
"image": {
"id": "sha256:abcxyz",
"deploymentCount": 1,
"deployments": [
{
"id": "mock-deployment-001",
"name": "central",
"type": "Deployment",
"clusterName": "remote",
"namespace": "stackrox",
"created": "2024-03-01T17:45:32.757812569Z",
"__typename": "Deployment"
}
],
"__typename": "Image"
}
}
}
85 changes: 85 additions & 0 deletions ui/apps/platform/cypress/integration-ocp/routes.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import { interceptAndWatchRequests, interceptRequests } from '../helpers/request';
import { hasFeatureFlag } from '../helpers/features';
import { toImageV2Response } from '../integration/vulnerabilities/workloadCves/WorkloadCves.helpers';

export const acsAuthNamespaceHeader = 'acs-auth-namespace-scope';

export const metadataRoute = 'metadata';
Expand Down Expand Up @@ -33,3 +37,84 @@ export const getCVEsForDeploymentRouteMatcher = {
method: 'POST',
url: '**/api-service/**/api/graphql?opname=getCvesForDeployment',
};

export function getOcpRouteMatcherMapForGraphQL<T extends string>(opnames: T[]) {
return Object.fromEntries(
opnames.map((opname) => [
opname,
{ method: 'POST' as const, url: `**/api-service/**/api/graphql?opname=${opname}` },
Comment thread
dvail marked this conversation as resolved.
])
) as Record<T, { method: 'POST'; url: string }>;
}

type FixtureMap = Record<string, { fixture: string } | { body: unknown }>;

export function interceptOcpGraphQL(fixtureMap: FixtureMap) {
interceptRequests(getOcpRouteMatcherMapForGraphQL(Object.keys(fixtureMap)), fixtureMap);
}

export function watchOcpGraphQL(fixtureMap: FixtureMap) {
return interceptAndWatchRequests(
getOcpRouteMatcherMapForGraphQL(Object.keys(fixtureMap)),
fixtureMap
);
}

export function interceptWorkloadCveFixtures() {
const isFlattenImageData = hasFeatureFlag('ROX_FLATTEN_IMAGE_DATA');

interceptOcpGraphQL({
getImageCVEList: { fixture: 'vulnerabilities/workloadCves/getImageCVEList.json' },
getImageCveMetadata: { fixture: 'vulnerabilities/workloadCves/getImageCveMetadata.json' },
getImageCveSummaryData: {
fixture: 'vulnerabilities/workloadCves/getImageCveSummaryData.json',
},
getImagesForCVE: { fixture: 'vulnerabilities/workloadCves/getImagesForCVE.json' },
getImageList: { fixture: 'vulnerabilities/workloadCves/getImageList.json' },
});

// When ROX_FLATTEN_IMAGE_DATA is enabled, queries return ImageV2 types instead of
// Image types. Fixtures use the v1 shape, so we transform them for v2 compatibility.
// getImageDetails uses an alias (image: imageV2) so the root key stays `image`,
// but __typename must be ImageV2 for the fragment to match.
// getCVEsForImage and getImageResources use `imageV2` as the root key directly.
if (isFlattenImageData) {
cy.fixture('vulnerabilities/workloadCves/imageWithMultipleCves.json').then((v1Response) => {
interceptOcpGraphQL({
getImageDetails: {
body: {
data: {
image: {
...v1Response.data.image,
__typename: 'ImageV2',
},
},
},
},
});
});

const imageV2Fixtures = [
{
opname: 'getCVEsForImage',
fixture: 'vulnerabilities/workloadCves/multipleCvesForImage.json',
},
{
opname: 'getImageResources',
fixture: 'vulnerabilities/workloadCves/getImageResources.json',
},
];

imageV2Fixtures.forEach(({ opname, fixture }) => {
cy.fixture(fixture).then((v1Response) => {
interceptOcpGraphQL({ [opname]: { body: toImageV2Response(v1Response) } });
});
});
} else {
interceptOcpGraphQL({
getImageDetails: { fixture: 'vulnerabilities/workloadCves/imageWithMultipleCves.json' },
getCVEsForImage: { fixture: 'vulnerabilities/workloadCves/multipleCvesForImage.json' },
getImageResources: { fixture: 'vulnerabilities/workloadCves/getImageResources.json' },
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { selectProject } from '../../helpers/ocpConsole';
import { assertSearchEntities } from '../../integration/vulnerabilities/workloadCves/WorkloadCves.helpers';
import { selectors } from '../../integration/vulnerabilities/workloadCves/WorkloadCves.selectors';
import { selectors as vulnerabilitiesSelectors } from '../../integration/vulnerabilities/vulnerabilities.selectors';
import { interceptWorkloadCveFixtures } from '../routes';

function visitFirstCve() {
withOcpAuth();
Expand All @@ -22,6 +23,10 @@ function visitFirstCve() {
}

describe('Security vulnerabilities - CVE Detail page', () => {
beforeEach(() => {
interceptWorkloadCveFixtures();
});

it('should navigate to the CVE Detail page and account for the project filter', () => {
visitFirstCve().then(() => {
selectProject('All Projects');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ 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';
import { hasFeatureFlag } from '../../helpers/features';
import { toImageV2Response } from '../../integration/vulnerabilities/workloadCves/WorkloadCves.helpers';
import { acsAuthNamespaceHeader, interceptWorkloadCveFixtures, watchOcpGraphQL } from '../routes';
import pf6 from '../../selectors/pf6';

function visitImageDetailPage() {
withOcpAuth();
Expand All @@ -24,29 +28,46 @@ function visitImageDetailPage() {
}

describe('Security vulnerabilities - Image Detail page', () => {
beforeEach(() => {
interceptWorkloadCveFixtures();
});

it('should show the appropriate table columns on the workload resources tab', () => {
visitImageDetailPage()
.then(() => {
cy.get('button[role="tab"]:contains("Resources")').click();
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
selectProject('All Projects');
const expectedColumns = ['Name', 'Namespace', 'Created'];
assertVisibleTableColumns('table', expectedColumns);
const resourcesFixture = 'vulnerabilities/workloadCves/getImageResources.json';
cy.fixture(resourcesFixture)
.then((v1Response) => {
const isFlattenImageData = hasFeatureFlag('ROX_FLATTEN_IMAGE_DATA');
Comment thread
dvail marked this conversation as resolved.
const resourcesFixtures = {
getImageResources: {
body: isFlattenImageData ? toImageV2Response(v1Response) : v1Response,
},
};

// 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);
return watchOcpGraphQL(resourcesFixtures);
})
.then(({ waitForRequests }) => {
// We manually set 'stackrox' as the namespace for the first request
waitForRequests(['getImageResources']).then((interception) => {
const req = Array.isArray(interception) ? interception[0] : interception;
expect(req.request.headers[acsAuthNamespaceHeader]).to.equal('stackrox');
});

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

// Change to 'All Projects' to test the 'Namespace' column
selectProject('All Projects');

waitForRequests(['getImageResources']).then((interception) => {
const req = Array.isArray(interception) ? interception[0] : interception;
expect(req.request.headers[acsAuthNamespaceHeader]).to.equal('*');
});

assertVisibleTableColumns('table', ['Name', 'Namespace', 'Created']);
});
});
Comment thread
dvail marked this conversation as resolved.
});

it('should navigate to the CVE Detail from the vulnerability table for the image', () => {
Expand All @@ -57,8 +78,9 @@ describe('Security vulnerabilities - Image Detail page', () => {
.click()
.then(([$cveLink]) => Promise.resolve($cveLink.innerText.replace('\n', '')));
})
.then((cveName) => {
cy.get('h1').contains(cveName);
.then(() => {
cy.get(pf6.card).contains('Affected images');
cy.get(pf6.card).contains('Images by severity');
Comment thread
dvail marked this conversation as resolved.
});
});
});
Loading