Skip to content
Open
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
373 changes: 373 additions & 0 deletions cypress/e2e/vulnerability.cy.js
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(

Copy link
Copy Markdown
Collaborator

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 ?

() =>
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`)
})
})
})
})
Loading