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
1 change: 1 addition & 0 deletions qa-tests-backend/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ dependencies {
implementation(libs.picocontainer)

implementation(libs.commons.codec)
implementation(libs.uuid.creator)

implementation(projects.annotations)
}
Expand Down
1 change: 1 addition & 0 deletions qa-tests-backend/gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ jaxb-runtime = { module = "org.glassfish.jaxb:jaxb-runtime", version = "4.0.7" }
javers-core = { module = "org.javers:javers-core", version = "7.11.0" }
picocontainer = { module = "org.picocontainer:picocontainer", version = "2.15.2" }
commons-codec = { module = "commons-codec:commons-codec", version = "1.20.0" }
uuid-creator = { module = "com.github.f4b6a3:uuid-creator", version = "6.1.1" }

[plugins]
protobuf = { id = "com.google.protobuf", version = "0.8.19" }
Expand Down
20 changes: 20 additions & 0 deletions qa-tests-backend/src/main/groovy/util/Helpers.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ package util

import java.nio.file.Path
import java.nio.file.Paths
import java.security.MessageDigest
import java.text.SimpleDateFormat

import com.github.f4b6a3.uuid.UuidCreator

import groovy.transform.CompileDynamic
import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j
Expand Down Expand Up @@ -194,6 +197,23 @@ class Helpers {
return false
}

// Mirrors Go's pkg/uuid.NewV5FromNonUUIDs: SHA-256 the namespace string to derive
// a UUID namespace, then produce a standard UUIDv5 (SHA-1) from that namespace and name.
static String newV5FromNonUUIDs(String ns, String name) {
byte[] sha256 = MessageDigest.getInstance("SHA-256").digest(ns.getBytes("UTF-8"))
UUID nsUUID = UuidCreator.fromBytes(Arrays.copyOf(sha256, 16))
return UuidCreator.getNameBasedSha1(nsUUID, name).toString()
}

// Mirrors Go's pkg/images/utils.NewImageV2ID: generates a deterministic UUIDv5
// from the image full name and digest.
static String generateImageV2ID(String fullName, String digest) {
if (!fullName || !digest) {
return ""
}
return newV5FromNonUUIDs(fullName, digest)
}

private static final Set<String> VOLATILE_ANNOTATIONS_TO_IGNORE = [
"machineconfiguration.openshift.io/lastSyncedControllerConfigResourceVersion"
].toSet()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@ class AdmissionControllerTest extends BaseSpecification {
// Pre-scan the image so Central has cached scan results for CVE-based policy evaluation.
ImageService.scanImage(SCAN_INLINE_IMAGE_NAME_WITH_SHA)

ImageOuterClass.Image image = ImageService.getImage(SCAN_INLINE_IMAGE_SHA, false)
def imageId = flattenImageDataEnabled ? TEST_IMAGE_V2_ID : SCAN_INLINE_IMAGE_SHA
ImageOuterClass.Image image = ImageService.getImage(imageId, false)
assert image
assert !image.getNotesList().contains(ImageOuterClass.Image.Note.MISSING_METADATA)

Expand Down
6 changes: 6 additions & 0 deletions qa-tests-backend/src/test/groovy/BaseSpecification.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ class BaseSpecification extends Specification {
static final String TEST_IMAGE = "quay.io/rhacs-eng/qa-multi-arch:nginx-2.0.3@$TEST_IMAGE_SHA"
static final String TEST_IMAGE_NAME_WITH_SHA = TEST_IMAGE
static final String TEST_IMAGE_SHA = "sha256:ebecc1ad41054eaef19ef9c84e0d95551dfbdebbf0875fd407aee697e4be3860"
// UUIDv5 of TEST_IMAGE (full name) and TEST_IMAGE_SHA (digest), used as image ID when FlattenImageData is enabled.
static final String TEST_IMAGE_V2_ID = Helpers.generateImageV2ID(TEST_IMAGE, TEST_IMAGE_SHA)

static final String RUN_ID

Expand Down Expand Up @@ -75,6 +77,7 @@ class BaseSpecification extends Specification {
public static String coreImageIntegrationId = null

public static boolean scannerV4Enabled = false
public static boolean flattenImageDataEnabled = false

private static synchronizedGlobalSetup() {
synchronized(BaseSpecification) {
Expand Down Expand Up @@ -134,6 +137,9 @@ class BaseSpecification extends Specification {
scannerV4Enabled = FeatureFlagService.isFeatureFlagEnabled("ROX_SCANNER_V4")
LOG.info "Scanner V4 enabled: ${scannerV4Enabled}"

flattenImageDataEnabled = Env.get("ROX_FLATTEN_IMAGE_DATA") == "true"
LOG.info "Flatten Image Data enabled: ${flattenImageDataEnabled}"

if (ClusterService.isOpenShift4()) {
assert Env.mustGetOrchestratorType() == OrchestratorTypes.OPENSHIFT,
"Set CLUSTER=OPENSHIFT when testing OpenShift"
Expand Down
20 changes: 16 additions & 4 deletions qa-tests-backend/src/test/groovy/CSVTest.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,19 @@ class CSVTest extends BaseSpecification {
return "CVE Type:IMAGE_CVE+"
}

def getTestImageId() {
return flattenImageDataEnabled ? TEST_IMAGE_V2_ID : TEST_IMAGE_SHA
}

def getDeploymentUid() {
return CVE_DEPLOYMENT.deploymentUid
}

def getFixableCvesInImageQuery() {
def imageFilter = flattenImageDataEnabled ? "Image ID" : "Image Sha"
return "${imageFilter}:${getTestImageId()}+Fixable:true"
}

Map<String, Object> payload(String id) {
def pagination = new Pagination(0, 0, new SortOption("cvss", true))
return [
Expand Down Expand Up @@ -227,10 +240,9 @@ class CSVTest extends BaseSpecification {
where:
"Data is"

description | id | query
"FIXABLE_CVES_IN_IMAGE_QUERY" | TEST_IMAGE_SHA | "Image Sha:${TEST_IMAGE_SHA}+Fixable:true"
"FIXABLE_CVES_IN_DEPLOYMENT_QUERY" | CVE_DEPLOYMENT.deploymentUid |
"Deployment ID:${CVE_DEPLOYMENT.deploymentUid}+Fixable:true"
description | id | query
"FIXABLE_CVES_IN_IMAGE_QUERY" | getTestImageId() | getFixableCvesInImageQuery()
"FIXABLE_CVES_IN_DEPLOYMENT_QUERY" | getDeploymentUid() | "Deployment ID:${getDeploymentUid()}+Fixable:true"
}

@EqualsAndHashCode(includeFields = true)
Expand Down
4 changes: 4 additions & 0 deletions qa-tests-backend/src/test/groovy/GlobalSearch.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ class GlobalSearch extends BaseSpecification {
SearchServiceOuterClass.SearchCategory.NAMESPACES,
SearchServiceOuterClass.SearchCategory.IMAGES,
SearchServiceOuterClass.SearchCategory.DEPLOYMENTS)
if (flattenImageDataEnabled) {
EXPECTED_DEPLOYMENT_CATEGORIES.add(SearchServiceOuterClass.SearchCategory.IMAGES_V2)
EXPECTED_IMAGE_CATEGORIES.add(SearchServiceOuterClass.SearchCategory.IMAGES_V2)
}

orchestrator.createDeployment(DEPLOYMENT)
assert Services.waitForDeployment(DEPLOYMENT)
Expand Down
6 changes: 4 additions & 2 deletions qa-tests-backend/src/test/groovy/ImageManagementTest.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,8 @@ class ImageManagementTest extends BaseSpecification {
then:
"Assert that riskScore is non-zero"
withRetry(10, 3) {
def image = ImageService.getImage(TEST_IMAGE_SHA)
def imageId = flattenImageDataEnabled ? TEST_IMAGE_V2_ID : TEST_IMAGE_SHA
def image = ImageService.getImage(imageId)
assert image != null && image.riskScore != 0
}
}
Expand All @@ -241,7 +242,8 @@ class ImageManagementTest extends BaseSpecification {
then:
"Assert that riskScore is non-zero"
withRetry(10, 3) {
def image = ImageService.getImage(TEST_IMAGE_SHA)
def imageId = flattenImageDataEnabled ? TEST_IMAGE_V2_ID : TEST_IMAGE_SHA
def image = ImageService.getImage(imageId)
assert image != null && image.riskScore != 0
}

Expand Down
2 changes: 1 addition & 1 deletion qa-tests-backend/src/test/groovy/SACTest.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -404,7 +404,7 @@ class SACTest extends BaseSpecification {
tokenName | category | numResults
NOACCESSTOKEN | "Cluster" | 0
"searchDeploymentsToken" | "Deployment" | 1
"searchImagesToken" | "Image" | 1
"searchImagesToken" | "Image" | (flattenImageDataEnabled ? 2 : 1)
}

@Unroll
Expand Down
22 changes: 17 additions & 5 deletions qa-tests-backend/src/test/groovy/UpgradesTest.groovy
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import static util.Helpers.withRetry

import com.google.protobuf.util.JsonFormat
import groovy.io.FileType
import io.grpc.StatusRuntimeException
Expand Down Expand Up @@ -84,8 +86,12 @@ class UpgradesTest extends BaseSpecification {
def nodes = NodeService.getNodes()
assert nodes.size() != 0
"Image API returns non-zero values on upgrade"
def images = ImageService.getImages()
assert images.size() != 0
// When FlattenImageData is enabled, the reprocessor moves images from the
// images table to images_v2 after upgrade, which may take some time.
withRetry(30, 10) {
def images = ImageService.getImages()
assert images.size() != 0
}
}

@Unroll
Expand All @@ -100,9 +106,15 @@ class UpgradesTest extends BaseSpecification {

then:
"Check that we got the correct number of #resourceType from GraphQL "
assert resultRet.getValue() != null
def items = resultRet.getValue()[resourceType]
assert items.size() >= minResults
// When FlattenImageData is enabled, the reprocessor moves images from the
// images table to images_v2 after upgrade, which may take some time.
withRetry(30, 10) {
def retryResultRet = gqlService.Call(getQuery(resourceType), [ query: searchQuery ])
assert retryResultRet.getCode() == 200
assert retryResultRet.getValue() != null
def items = retryResultRet.getValue()[resourceType]
assert items.size() >= minResults
}

where:
"Data Inputs Are:"
Expand Down
31 changes: 18 additions & 13 deletions qa-tests-backend/src/test/groovy/VulnMgmtTest.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import io.stackrox.proto.storage.Cve.VulnerabilitySeverity
import org.junit.Assume
import services.GraphQLService
import services.ImageService
import util.Helpers

import spock.lang.Tag
import spock.lang.Unroll
Expand All @@ -29,6 +30,9 @@ class VulnMgmtTest extends BaseSpecification {
static final private String UBUNTU_IMAGE =
"quay.io/rhacs-eng/qa:ubuntu-22.04-amd64"

static final private MODERATE = VulnerabilitySeverity.MODERATE_VULNERABILITY_SEVERITY
static final private LOW = VulnerabilitySeverity.LOW_VULNERABILITY_SEVERITY

private static final EMBEDDED_IMAGE_QUERY = """
query getImage(\$id: ID!, \$query: String) {
result: fullImage(id: \$id) {
Expand Down Expand Up @@ -187,14 +191,14 @@ query getComponentId(\$imageId: ID!, \$componentQuery: String) {
Assume.assumeFalse(scannerV4Enabled)

expect:
verifySeveritiesAndCvss(imageDigest, component, cve, severity, cvss)
verifySeveritiesAndCvss(imageDigest, imageName, component, cve, severity, cvss)

where:
"Data inputs are: "

imageDigest | component | cve | severity | cvss
RHEL_IMAGE_LEGACY_DIGEST | "glib2" | "CVE-2019-13012" | VulnerabilitySeverity.LOW_VULNERABILITY_SEVERITY | 4.4
UBUNTU_IMAGE_DIGEST | "gnupg2" | "CVE-2022-3219" | VulnerabilitySeverity.LOW_VULNERABILITY_SEVERITY | 3.3
imageDigest | imageName | component | cve | severity | cvss
RHEL_IMAGE_LEGACY_DIGEST | RHEL_IMAGE_LEGACY | "glib2" | "CVE-2019-13012" | LOW | 4.4
UBUNTU_IMAGE_DIGEST | UBUNTU_IMAGE | "gnupg2" | "CVE-2022-3219" | LOW | 3.3
}

@Unroll
Expand All @@ -203,27 +207,28 @@ query getComponentId(\$imageId: ID!, \$componentQuery: String) {
Assume.assumeTrue(scannerV4Enabled)

expect:
verifySeveritiesAndCvss(imageDigest, component, cve, severity, cvss)
verifySeveritiesAndCvss(imageDigest, imageName, component, cve, severity, cvss)

where:
"Data inputs are: "

imageDigest | component | cve | severity | cvss
RHEL_IMAGE_DIGEST | "python3" | "CVE-2025-11468" | VulnerabilitySeverity.MODERATE_VULNERABILITY_SEVERITY | 4.5
UBUNTU_IMAGE_DIGEST | "gpgv" | "CVE-2022-3219" | VulnerabilitySeverity.LOW_VULNERABILITY_SEVERITY | 3.3
imageDigest | imageName | component | cve | severity | cvss
RHEL_IMAGE_DIGEST | RHEL_IMAGE | "python3" | "CVE-2025-11468" | MODERATE | 4.5
UBUNTU_IMAGE_DIGEST | UBUNTU_IMAGE | "gpgv" | "CVE-2022-3219" | LOW | 3.3
}

private void verifySeveritiesAndCvss(String imageDigest, String component, String cve,
private void verifySeveritiesAndCvss(String imageDigest, String imageName, String component, String cve,
VulnerabilitySeverity severity, double cvss) {
def gqlService = new GraphQLService()

def query = "CVE:${cve}"
def imageId = flattenImageDataEnabled ? Helpers.generateImageV2ID(imageName, imageDigest) : imageDigest

// Fetch the component ID dynamically since IDs now include image ID and index
def componentID = getComponentIDForImage(gqlService, imageDigest, component)
def componentID = getComponentIDForImage(gqlService, imageId, component)

def embeddedImageRes = gqlService.Call(getEmbeddedImageQuery(),
[id: imageDigest, query: query])
[id: imageId, query: query])

// Expanded instead of using hasErrors() for easier debugging if there are errors
// as the test framework will actually print out the errors now
Expand All @@ -235,12 +240,12 @@ query getComponentId(\$imageId: ID!, \$componentQuery: String) {
def embeddedImageResVuln = embeddedImageRes.value.result.scan.imageComponents[0].imageVulnerabilities[0]

def topLevelImageRes = gqlService.Call(getTopLevelImageQuery(),
[id: imageDigest, query: query])
[id: imageId, query: query])
assert topLevelImageRes.hasNoErrors()
def topLevelImageResVuln = topLevelImageRes.value.result.vulns[0]

def fixableCVEImageRes = gqlService.Call(getImageFixableCVEQuery(),
[id: imageDigest, vulnQuery: query, scopeQuery: "Image SHA:${imageDigest}"])
[id: imageId, vulnQuery: query, scopeQuery: "Image SHA:${imageDigest}"])
assert fixableCVEImageRes.hasNoErrors()
def fixableCVEImageResVuln = fixableCVEImageRes.value.result.vulnerabilities[0]

Expand Down
Loading