-
Notifications
You must be signed in to change notification settings - Fork 4
EDM-4253: Add UI Vulnerability coverage #30
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
amalykhi
wants to merge
6
commits into
flightctl:main
Choose a base branch
from
amalykhi:EDM-4253
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+677
−1
Open
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
350f2bd
EDM-4253: Add Trustify vulnerability integration for UI tests
amalykhi a8fe2cd
Merge branch 'flightctl:main' into EDM-4253
amalykhi c752aed
Fix organization selection for all Cypress tests
amalykhi fdc8ea0
Fix organization selection timing issue causing 70-second delays
amalykhi 61f8263
Revert "Fix organization selection timing issue causing 70-second del…
amalykhi 6c5a00b
Fix org selection timing by removing premature call from ensureLoggedIn
amalykhi File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,373 @@ | ||
| import { securityPage, KNOWN_VULNERABLE_IMAGE } from '../views/securityPage' | ||
| import { devicesPage } from '../views/devicesPage' | ||
| import { common } from '../views/common' | ||
|
|
||
| /** | ||
| * Vulnerability Reporting End-to-End Tests | ||
| * | ||
| * Prerequisites: | ||
| * - Jenkins job must run with flightctl_trustify_enabled: true | ||
| * - Trustify must have SBOM data for the vulnerable test image | ||
| * - At least one device must be enrolled and available for testing | ||
| * | ||
| * Test Image Details: | ||
| * - Image: quay.io/centos-bootc/centos-bootc:stream9 | ||
| * - Digest: sha256:0395113f05c2fe5544ddb693603abaaee36f71d46c810526efd270327db6b8bd | ||
| * - CVEs: 7 total (1 Critical, 1 High, 4 Medium, 1 Low) | ||
| */ | ||
|
|
||
| describe('Vulnerability Reporting', () => { | ||
| const FLEET_NAME = 'test-fleet-vuln-' + Date.now() | ||
| let TEST_DEVICE_NAME = null | ||
|
|
||
| const API = Cypress.env('FLIGHTCTL_API') | ||
|
|
||
| before(() => { | ||
| cy.ensureLoggedIn() | ||
|
|
||
| // Find an enrolled device to use for testing | ||
| cy.request(`${API}/api/v1/devices`).then((resp) => { | ||
| const devices = resp.body.items || [] | ||
| const enrolledDevices = devices.filter((d) => d.metadata.labels?.enrollment !== 'pending') | ||
|
|
||
| if (enrolledDevices.length === 0) { | ||
| throw new Error('No enrolled devices available for vulnerability testing') | ||
| } | ||
|
|
||
| TEST_DEVICE_NAME = enrolledDevices[0].metadata.name | ||
| cy.log(`Using device: ${TEST_DEVICE_NAME}`) | ||
| }) | ||
| }) | ||
|
|
||
| after(() => { | ||
| // Cleanup: Delete test fleet if it exists | ||
| if (FLEET_NAME) { | ||
| cy.request({ | ||
| method: 'DELETE', | ||
| url: `${API}/api/v1/fleets/${FLEET_NAME}`, | ||
| failOnStatusCode: false, | ||
| }) | ||
| } | ||
|
|
||
| // Cleanup: Remove fleet label from device if it was added | ||
| if (TEST_DEVICE_NAME) { | ||
| cy.request({ | ||
| method: 'PATCH', | ||
| url: `${API}/api/v1/devices/${TEST_DEVICE_NAME}`, | ||
| body: { | ||
| metadata: { | ||
| labels: { | ||
| fleet: null, | ||
| }, | ||
| }, | ||
| }, | ||
| failOnStatusCode: false, | ||
| }) | ||
| } | ||
| }) | ||
|
|
||
| /** | ||
| * Helper: Wait for device status to be UpToDate | ||
| */ | ||
| const waitForDeviceUpToDate = (deviceName, timeout = 300000) => { | ||
| cy.log(`Waiting for device ${deviceName} to reach UpToDate status...`) | ||
| cy.waitUntil( | ||
| () => | ||
| cy.request(`${API}/api/v1/devices/${deviceName}`).then((resp) => { | ||
| const status = resp.body?.status?.updated?.status | ||
| cy.log(`Device status: ${status}`) | ||
| return status === 'UpToDate' | ||
| }), | ||
| { | ||
| timeout, | ||
| interval: 5000, | ||
| errorMsg: `Device ${deviceName} did not reach UpToDate status within ${timeout}ms`, | ||
| }, | ||
| ) | ||
| } | ||
|
|
||
| /** | ||
| * Helper: Wait for device to report specific digest | ||
| */ | ||
| const waitForDeviceDigest = (deviceName, expectedDigest, timeout = 300000) => { | ||
| cy.log(`Waiting for device ${deviceName} to update to digest ${expectedDigest}...`) | ||
| cy.waitUntil( | ||
| () => | ||
| cy.request(`${API}/api/v1/devices/${deviceName}`).then((resp) => { | ||
| const currentDigest = resp.body?.status?.os?.imageDigest | ||
| cy.log(`Current digest: ${currentDigest}`) | ||
| return currentDigest === expectedDigest | ||
| }), | ||
| { | ||
| timeout, | ||
| interval: 5000, | ||
| errorMsg: `Device did not update to digest ${expectedDigest} within ${timeout}ms`, | ||
| }, | ||
| ) | ||
| // Wait extra 15s for vulnerability sync (runs every 10s) | ||
| cy.log('Waiting additional 15s for vulnerability sync...') | ||
| cy.wait(15000) | ||
| } | ||
|
|
||
| describe('Initial State - No Vulnerabilities', () => { | ||
| it('should show 0 vulnerabilities on Overview page', () => { | ||
| cy.visit('/overview') | ||
| securityPage.expectSecurityOverviewVisible() | ||
| securityPage.expectNoVulnerabilities() | ||
| }) | ||
|
|
||
| it('should show 0 vulnerabilities on Device page', () => { | ||
| cy.visit(`/devices/${TEST_DEVICE_NAME}`) | ||
| cy.wait(1000) // Allow page to load | ||
|
|
||
| // Check if Security overview section exists and shows no vulnerabilities | ||
| cy.get('body').then(($body) => { | ||
| if ($body.text().includes('Security overview')) { | ||
| securityPage.expectNoVulnerabilities() | ||
| } else { | ||
| cy.log('Security overview section not present yet') | ||
| } | ||
| }) | ||
| }) | ||
| }) | ||
|
|
||
| describe('Create Fleet with Vulnerable Image', () => { | ||
| it('should create a fleet with the vulnerable CentOS image', () => { | ||
| cy.request('POST', `${API}/api/v1/fleets`, { | ||
| apiVersion: 'v1alpha1', | ||
| kind: 'Fleet', | ||
| metadata: { | ||
| name: FLEET_NAME, | ||
| }, | ||
| spec: { | ||
| template: { | ||
| spec: { | ||
| os: { | ||
| image: KNOWN_VULNERABLE_IMAGE.image, | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| }).then((response) => { | ||
| expect(response.status).to.eq(201) | ||
| cy.log(`Fleet ${FLEET_NAME} created successfully`) | ||
| }) | ||
| }) | ||
|
|
||
| it('should verify fleet shows 0 CVEs before device attachment', () => { | ||
| cy.visit(`/fleets/${FLEET_NAME}`) | ||
| cy.wait(1000) | ||
|
|
||
| // Fleet has vulnerable image spec, but no devices running it yet | ||
| cy.get('body').then(($body) => { | ||
| if ($body.text().includes('Security overview')) { | ||
| securityPage.expectNoVulnerabilities() | ||
| } else { | ||
| cy.log('Security overview section not present yet on fleet') | ||
| } | ||
| }) | ||
| }) | ||
| }) | ||
|
|
||
| describe('Attach Device to Fleet and Wait for CVEs', () => { | ||
| it('should attach device to vulnerable fleet', () => { | ||
| cy.request('PATCH', `${API}/api/v1/devices/${TEST_DEVICE_NAME}`, { | ||
| metadata: { | ||
| labels: { | ||
| fleet: FLEET_NAME, | ||
| }, | ||
| }, | ||
| }).then((response) => { | ||
| expect(response.status).to.eq(200) | ||
| cy.log(`Device ${TEST_DEVICE_NAME} attached to fleet ${FLEET_NAME}`) | ||
| }) | ||
| }) | ||
|
|
||
| it('should wait for device to pull vulnerable image and reach UpToDate', () => { | ||
| // Wait for device to pull the new image | ||
| waitForDeviceDigest(TEST_DEVICE_NAME, KNOWN_VULNERABLE_IMAGE.digest) | ||
|
|
||
| // Wait for device status to be UpToDate | ||
| waitForDeviceUpToDate(TEST_DEVICE_NAME) | ||
|
|
||
| cy.log('Device is now running the vulnerable image and is UpToDate') | ||
| }) | ||
| }) | ||
|
|
||
| describe('Verify CVEs on Device Page', () => { | ||
| beforeEach(() => { | ||
| cy.visit(`/devices/${TEST_DEVICE_NAME}`) | ||
| cy.wait(2000) // Allow vulnerability data to load | ||
| }) | ||
|
|
||
| it('should display total vulnerability count', () => { | ||
| securityPage.expectVulnerabilityCount(KNOWN_VULNERABLE_IMAGE.totalCount) | ||
| }) | ||
|
|
||
| it('should display severity breakdown', () => { | ||
| securityPage.expectSeverityCounts(KNOWN_VULNERABLE_IMAGE.counts) | ||
| }) | ||
|
|
||
| it('should display Critical CVEs', () => { | ||
| securityPage.expectCvesVisible(KNOWN_VULNERABLE_IMAGE.cves.critical) | ||
| }) | ||
|
|
||
| it('should display High CVEs', () => { | ||
| securityPage.expectCvesVisible(KNOWN_VULNERABLE_IMAGE.cves.high) | ||
| }) | ||
|
|
||
| it('should open CVE details panel when clicking on a CVE', () => { | ||
| const criticalCve = KNOWN_VULNERABLE_IMAGE.cves.critical[0] | ||
|
|
||
| securityPage.clickCve(criticalCve) | ||
| securityPage.expectCveDetailsVisible() | ||
| securityPage.expectSeverityInDetails('Critical') | ||
| securityPage.expectPackageInDetails('glibc') | ||
| securityPage.closeCveDetails() | ||
| }) | ||
|
|
||
| it('should filter vulnerabilities by Critical severity', () => { | ||
| securityPage.filterBySeverity('Critical') | ||
| securityPage.expectTableRowCount(KNOWN_VULNERABLE_IMAGE.counts.critical) | ||
| securityPage.expectCveVisible(KNOWN_VULNERABLE_IMAGE.cves.critical[0]) | ||
| securityPage.clearFilters() | ||
| }) | ||
|
|
||
| it('should filter vulnerabilities by High severity', () => { | ||
| securityPage.filterBySeverity('High') | ||
| securityPage.expectTableRowCount(KNOWN_VULNERABLE_IMAGE.counts.high) | ||
| securityPage.expectCveVisible(KNOWN_VULNERABLE_IMAGE.cves.high[0]) | ||
| securityPage.clearFilters() | ||
| }) | ||
|
|
||
| it('should search for specific CVE by ID', () => { | ||
| const searchCve = KNOWN_VULNERABLE_IMAGE.cves.critical[0] | ||
| securityPage.searchVulnerabilities(searchCve) | ||
| securityPage.expectCveVisible(searchCve) | ||
| securityPage.clearSearch() | ||
| }) | ||
| }) | ||
|
|
||
| describe('Verify CVEs on Fleet Page', () => { | ||
| beforeEach(() => { | ||
| cy.visit(`/fleets/${FLEET_NAME}`) | ||
| cy.wait(2000) | ||
| }) | ||
|
|
||
| it('should display total vulnerability count on fleet', () => { | ||
| securityPage.expectVulnerabilityCount(KNOWN_VULNERABLE_IMAGE.totalCount) | ||
| }) | ||
|
|
||
| it('should display severity breakdown on fleet', () => { | ||
| securityPage.expectSeverityCounts(KNOWN_VULNERABLE_IMAGE.counts) | ||
| }) | ||
|
|
||
| it('should display CVEs on fleet page', () => { | ||
| securityPage.expectCveVisible(KNOWN_VULNERABLE_IMAGE.cves.critical[0]) | ||
| securityPage.expectCveVisible(KNOWN_VULNERABLE_IMAGE.cves.high[0]) | ||
| }) | ||
|
|
||
| it('should open CVE details from fleet page', () => { | ||
| const highCve = KNOWN_VULNERABLE_IMAGE.cves.high[0] | ||
|
|
||
| securityPage.clickCve(highCve) | ||
| securityPage.expectCveDetailsVisible() | ||
| securityPage.expectSeverityInDetails('High') | ||
| securityPage.closeCveDetails() | ||
| }) | ||
|
|
||
| it('should filter vulnerabilities on fleet page', () => { | ||
| securityPage.filterBySeverity('High') | ||
| securityPage.expectCveVisible(KNOWN_VULNERABLE_IMAGE.cves.high[0]) | ||
| securityPage.clearFilters() | ||
| }) | ||
| }) | ||
|
|
||
| describe('Verify CVEs on Overview Page', () => { | ||
| beforeEach(() => { | ||
| cy.visit('/overview') | ||
| cy.wait(2000) | ||
| }) | ||
|
|
||
| it('should display aggregated vulnerabilities on overview', () => { | ||
| securityPage.expectVulnerabilityCount(KNOWN_VULNERABLE_IMAGE.totalCount) | ||
| }) | ||
|
|
||
| it('should display severity breakdown on overview', () => { | ||
| securityPage.expectSeverityCounts(KNOWN_VULNERABLE_IMAGE.counts) | ||
| }) | ||
| }) | ||
|
|
||
| describe('Detach Device and Verify CVEs Disappear', () => { | ||
| it('should detach device from fleet', () => { | ||
| cy.request('PATCH', `${API}/api/v1/devices/${TEST_DEVICE_NAME}`, { | ||
| metadata: { | ||
| labels: { | ||
| fleet: null, | ||
| }, | ||
| }, | ||
| }).then((response) => { | ||
| expect(response.status).to.eq(200) | ||
| cy.log(`Device ${TEST_DEVICE_NAME} detached from fleet ${FLEET_NAME}`) | ||
| }) | ||
| }) | ||
|
|
||
| it('should wait for device to rollback from vulnerable image', () => { | ||
| cy.log('Waiting for device to rollback to original image...') | ||
| cy.waitUntil( | ||
| () => | ||
| cy.request(`${API}/api/v1/devices/${TEST_DEVICE_NAME}`).then((resp) => { | ||
| const currentDigest = resp.body?.status?.os?.imageDigest | ||
| const hasRolledBack = currentDigest !== KNOWN_VULNERABLE_IMAGE.digest | ||
| if (hasRolledBack) { | ||
| cy.log(`Device rolled back to: ${currentDigest}`) | ||
| } | ||
| return hasRolledBack | ||
| }), | ||
| { | ||
| timeout: 300000, | ||
| interval: 5000, | ||
| errorMsg: 'Device did not rollback from vulnerable image', | ||
| }, | ||
| ) | ||
|
|
||
| // Wait for device to be UpToDate again | ||
| waitForDeviceUpToDate(TEST_DEVICE_NAME) | ||
|
|
||
| // Wait for vulnerability sync to clear | ||
| cy.log('Waiting for vulnerability sync to clear...') | ||
| cy.wait(15000) | ||
| }) | ||
|
|
||
| it('should verify CVEs disappeared from Device page', () => { | ||
| cy.visit(`/devices/${TEST_DEVICE_NAME}`) | ||
| cy.wait(2000) | ||
|
|
||
| securityPage.expectNoVulnerabilities() | ||
| }) | ||
|
|
||
| it('should verify CVEs disappeared from Fleet page', () => { | ||
| cy.visit(`/fleets/${FLEET_NAME}`) | ||
| cy.wait(2000) | ||
|
|
||
| // Fleet has no devices attached now, should show 0 CVEs | ||
| securityPage.expectNoVulnerabilities() | ||
| }) | ||
|
|
||
| it('should verify CVEs disappeared from Overview page', () => { | ||
| cy.visit('/overview') | ||
| cy.wait(2000) | ||
|
|
||
| securityPage.expectNoVulnerabilities() | ||
| }) | ||
| }) | ||
|
|
||
| describe('Cleanup', () => { | ||
| it('should delete test fleet', () => { | ||
| cy.request('DELETE', `${API}/api/v1/fleets/${FLEET_NAME}`).then((response) => { | ||
| expect(response.status).to.eq(200) | ||
| cy.log(`Fleet ${FLEET_NAME} deleted successfully`) | ||
| }) | ||
| }) | ||
| }) | ||
| }) | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is waitUntil? where is it coming from ?