diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 11eb1c5..512ef97 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,9 +4,6 @@ updates: directory: '/' schedule: interval: 'weekly' - ignore: - - dependency-name: "*" - update-types: ["version-update:semver-major"] - package-ecosystem: 'github-actions' directory: '/' diff --git a/build.gradle b/build.gradle index 27f464e..cf2c0ea 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'org.springframework.boot' version '3.5.9' + id 'org.springframework.boot' version '4.0.1' id 'io.spring.dependency-management' version '1.1.7' id 'org.asciidoctor.jvm.convert' version '4.0.5' } @@ -17,42 +17,48 @@ repositories { } ext { - set('snippetsDir', file("build/generated-snippets")) + set('snippetsDir', file('build/generated-snippets')) } configurations { asciidoctorExtensions } +asciidoctorj { + version = '3.0.0' +} + dependencies { asciidoctorExtensions 'org.springframework.restdocs:spring-restdocs-asciidoctor' implementation 'org.springframework.boot:spring-boot-starter-actuator' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-web' - implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.15' + implementation 'org.springframework.boot:spring-boot-starter-webmvc' implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' implementation 'org.commonmark:commonmark:0.27.0' + implementation 'org.apache.commons:commons-lang3:3.20.0' runtimeOnly 'org.postgresql:postgresql' testImplementation 'org.springframework.boot:spring-boot-starter-test' - testImplementation 'org.springframework.restdocs:spring-restdocs-restassured' - testImplementation 'io.rest-assured:rest-assured:5.5.6' - testImplementation 'org.junit.jupiter:junit-jupiter:6.0.1' + testImplementation 'org.springframework.boot:spring-boot-starter-data-jpa-test' + testImplementation 'org.springframework.boot:spring-boot-starter-webmvc-test' + testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } tasks.named('test') { - outputs.dir snippetsDir useJUnitPlatform() + outputs.dir snippetsDir } tasks.named('asciidoctor') { - configurations "asciidoctorExtensions" + configurations 'asciidoctorExtensions' inputs.dir snippetsDir dependsOn test } -bootJar { + +tasks.named('bootJar') { dependsOn asciidoctor - from("${asciidoctor.outputDir}/html5") { + from("${asciidoctor.outputDir}") { into 'static/docs' } } diff --git a/src/main/java/io/gardenlinux/glvd/db/NvdCveDataAttributeConverter.java b/src/main/java/io/gardenlinux/glvd/db/NvdCveDataAttributeConverter.java index 4b5706f..8e3a74b 100644 --- a/src/main/java/io/gardenlinux/glvd/db/NvdCveDataAttributeConverter.java +++ b/src/main/java/io/gardenlinux/glvd/db/NvdCveDataAttributeConverter.java @@ -1,20 +1,20 @@ package io.gardenlinux.glvd.db; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.persistence.AttributeConverter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import tools.jackson.core.JacksonException; +import tools.jackson.databind.json.JsonMapper; public class NvdCveDataAttributeConverter implements AttributeConverter { - private static final ObjectMapper objectMapper = new ObjectMapper(); + private static final JsonMapper objectMapper = new JsonMapper(); Logger logger = LoggerFactory.getLogger(NvdCveDataAttributeConverter.class); @Override public String convertToDatabaseColumn(NvdCve.Data attribute) { try { return objectMapper.writeValueAsString(attribute); - } catch (JsonProcessingException jpe) { + } catch (JacksonException jpe) { logger.warn("Cannot convert CVE Data into JSON"); return null; } @@ -24,7 +24,7 @@ public String convertToDatabaseColumn(NvdCve.Data attribute) { public NvdCve.Data convertToEntityAttribute(String dbData) { try { return objectMapper.readValue(dbData, NvdCve.Data.class); - } catch (JsonProcessingException e) { + } catch (JacksonException e) { logger.warn("Cannot convert JSON into CVE Data {}", dbData); return null; } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 8919e8a..b16e17c 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,9 +1,10 @@ spring.application.name=glvd spring.datasource.url=jdbc:postgresql://localhost:5432/glvd +spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect spring.datasource.username=glvd spring.datasource.password=glvd spring.sql.init.mode=never jakarta.persistence.query.timeout=5000 server.error.whitelabel.enabled=false management.endpoints.access.default=none -management.endpoint.health.access=read-only \ No newline at end of file +management.endpoint.health.access=read-only diff --git a/src/test/java/io/gardenlinux/glvd/ActuatorEndpointTests.java b/src/test/java/io/gardenlinux/glvd/ActuatorEndpointTests.java index ca74cd8..58c8817 100644 --- a/src/test/java/io/gardenlinux/glvd/ActuatorEndpointTests.java +++ b/src/test/java/io/gardenlinux/glvd/ActuatorEndpointTests.java @@ -1,53 +1,47 @@ package io.gardenlinux.glvd; -import io.restassured.RestAssured; -import io.restassured.builder.RequestSpecBuilder; -import io.restassured.specification.RequestSpecification; -import org.apache.http.HttpStatus; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.web.server.LocalServerPort; -import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; -import static io.restassured.RestAssured.given; import static org.hamcrest.Matchers.is; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -@ExtendWith({SpringExtension.class}) -public class ActuatorEndpointTests { +@SpringBootTest +class ActuatorEndpointTests { - @LocalServerPort - private Integer port; + @Autowired + private WebApplicationContext context; - private RequestSpecification spec; + private MockMvc mockMvc; - @BeforeEach - void setUp() { - this.spec = new RequestSpecBuilder().build(); - - RestAssured.baseURI = "http://localhost:" + port; - } + @BeforeEach + public void setUp() { + this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context).build(); + } // We want to use actuator for k8s liveness and readiness probes - @Test - public void shouldReturnHealth() { - given(this.spec).accept("application/json") - .when().port(this.port).get("/actuator/health") - .then().statusCode(HttpStatus.SC_OK) - .body("status", is("UP")); - } + @Test + void shouldReturnHealth() throws Exception { + this.mockMvc.perform(get("/actuator/health")) + .andExpect(status().isOk()) + .andExpect(jsonPath("status", is("UP"))); + } @ParameterizedTest @ValueSource(strings = {"prometheus", "metrics", "env", "heapdump", "beans", "loggers", "mappings", "shutdown"}) - public void shouldNotReturnSensitiveEndpoints(String endpoint) { - given(this.spec).accept("application/json") - .when().port(this.port).get("/actuator/" + endpoint) - .then().statusCode(HttpStatus.SC_NOT_FOUND) - .body("path", is("/actuator/" + endpoint)); + public void shouldNotReturnSensitiveEndpoints(String endpoint) throws Exception { + this.mockMvc.perform(get("/actuator/" + endpoint)) + .andExpect(status().isNotFound()) + .andExpect(status().reason("No static resource actuator/" + endpoint + ".")); } } diff --git a/src/test/java/io/gardenlinux/glvd/GlvdControllerTest.java b/src/test/java/io/gardenlinux/glvd/GlvdControllerTest.java index 6584bfc..3906b3c 100644 --- a/src/test/java/io/gardenlinux/glvd/GlvdControllerTest.java +++ b/src/test/java/io/gardenlinux/glvd/GlvdControllerTest.java @@ -1,474 +1,403 @@ package io.gardenlinux.glvd; -import io.restassured.RestAssured; -import io.restassured.builder.RequestSpecBuilder; -import io.restassured.specification.RequestSpecification; -import org.apache.http.HttpStatus; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.restdocs.RestDocumentationContextProvider; import org.springframework.restdocs.RestDocumentationExtension; -import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultHandler; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; -import java.util.List; - -import static io.restassured.RestAssured.given; import static org.hamcrest.Matchers.*; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.put; import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; -import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; -import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -@ExtendWith({RestDocumentationExtension.class, SpringExtension.class}) +@SpringBootTest +@ExtendWith(RestDocumentationExtension.class) class GlvdControllerTest { + @Autowired + private WebApplicationContext context; - @LocalServerPort - private Integer port; + private MockMvc mockMvc; - private RequestSpecification spec; + static ResultHandler doc(String identifier) { + return document( + identifier, + preprocessRequest(modifyUris().host("security.gardenlinux.org").removePort()), + preprocessResponse(prettyPrint()) + ); + } @BeforeEach - void setUp(RestDocumentationContextProvider restDocumentation) { - this.spec = new RequestSpecBuilder().addFilter(documentationConfiguration(restDocumentation)).build(); - - RestAssured.baseURI = "http://localhost:" + port; + public void setUp(RestDocumentationContextProvider restDocumentation) { + this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(documentationConfiguration(restDocumentation).snippets().withAdditionalDefaults()).build(); } @Test - public void shouldReturnCvesForGardenlinuxImage() { - given(this.spec).accept("application/json") - .filter(document("getCveForImage", - preprocessRequest(modifyUris().scheme("https").host("security.gardenlinux.org").removePort()), - preprocessResponse(prettyPrint()))) - .when().port(this.port).get("/v1/cves/1592.4/image/azure-gardener_prod") - .then().statusCode(HttpStatus.SC_OK) - .body("sourcePackageName", hasItems("curl", "rsync", "jinja2", "python3.12", "linux")); + void shouldReturnCvesForGardenlinuxImage() throws Exception { + this.mockMvc.perform(get("/v1/cves/1592.4/image/azure-gardener_prod")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[*].sourcePackageName", hasItems("curl", "rsync", "jinja2", "python3.12", "linux"))) + .andDo(doc("getCveForImage")); } @Test - public void shouldReturnCvesForGardenlinux() { - given(this.spec).accept("application/json") - .filter(document("getCveForDistro", - preprocessRequest(modifyUris().scheme("https").host("security.gardenlinux.org").removePort()), - preprocessResponse(prettyPrint()))) - .when().port(this.port).get("/v1/cves/1592.4?sortBy=cveId&sortOrder=DESC") - .then().statusCode(HttpStatus.SC_OK) - .body("sourcePackageName", hasItems("curl", "rsync", "jinja2", "python3.12", "linux")); + void shouldReturnCvesForGardenlinux() throws Exception { + this.mockMvc.perform(get("/v1/cves/1592.4?sortBy=cveId&sortOrder=DESC")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[*].sourcePackageName", hasItems("curl", "rsync", "jinja2", "python3.12", "linux"))) + .andDo(doc("getCveForDistro")); } @Test - public void shouldReturnCvesForGardenlinuxShouldNotReturnKernelCveMarkedAsResolved() { - given(this.spec).accept("application/json") - .when().port(this.port).get("/v1/cves/1592.5?sortBy=cveId&sortOrder=DESC") - .then().statusCode(HttpStatus.SC_OK) - .body("cveId", hasItems("CVE-2025-0938", "CVE-2025-21864", "CVE-2024-44953")) + void shouldReturnCvesForGardenlinuxShouldNotReturnKernelCveMarkedAsResolved() throws Exception { + this.mockMvc.perform(get("/v1/cves/1592.5?sortBy=cveId&sortOrder=DESC")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", hasSize(3))) + .andExpect(jsonPath("$[*].cveId", contains("CVE-2025-0938", "CVE-2025-21864", "CVE-2024-44953"))) // CVE-2024-44953 is actually vulnerable, but marked as resolved via cve_context, thus it must return 'false' here - .body("vulnerable", hasItems(true, true, false)) - .body("vulnStatus", hasItems("Received", "Analyzed", "Modified")); + .andExpect(jsonPath("$[*].vulnerable", contains(false, true, false))) + .andExpect(jsonPath("$[*].vulnStatus", contains("Received", "Analyzed", "Modified"))); } @Test - public void shouldReturnCvesForGardenlinuxWithRejectedCve() { - given(this.spec).accept("application/json") - .when().port(this.port).get("/v1/cves/1592.7?sortBy=cveId&sortOrder=DESC") - .then().statusCode(HttpStatus.SC_OK) + void shouldReturnCvesForGardenlinuxWithRejectedCve() throws Exception { + this.mockMvc.perform(get("/v1/cves/1592.7?sortBy=cveId&sortOrder=DESC")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", hasSize(3))) // purposefully does not contain CVE-2025-8197 because it is 'rejected' - .body("size()", is(3)) - .body("cveId", hasItems("CVE-2024-35176", "CVE-2023-28755", "CVE-2024-44953")) - .body("cveId", not(hasItems("CVE-2025-8197"))) - .body("vulnerable", hasItems(true, true, true)) - .body("vulnStatus", hasItems("Awaiting Analysis", "Modified", "Modified")); + .andExpect(jsonPath("$[*].cveId", hasItems("CVE-2024-35176", "CVE-2023-28755", "CVE-2024-44953"))) + .andExpect(jsonPath("$[*].cveId", not(hasItem("CVE-2025-8197")))) + .andExpect(jsonPath("$[*].vulnerable", hasItems(true, true, true))) + .andExpect(jsonPath("$[*].vulnStatus", hasItems("Awaiting Analysis", "Modified", "Modified"))) + .andDo(doc("getCveForDistroWithRejectedCve")); } @Test - public void shuldReturnKernelCvesForGardenLinuxByPackageNameAndMarkResolvedCveAsNotVulnerable() { - given(this.spec).accept("application/json") - .when().port(this.port).get("/v1/cves/1592.5/packages/linux") - .then().statusCode(HttpStatus.SC_OK) - .body("cveId", hasItems("CVE-2025-21864", "CVE-2024-44953")) + void shouldReturnKernelCvesForGardenLinuxByPackageNameAndMarkResolvedCveAsNotVulnerable() throws Exception { + this.mockMvc.perform(get("/v1/cves/1592.5/packages/linux")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[*].cveId", hasItems("CVE-2025-21864", "CVE-2024-44953"))) // CVE-2024-44953 is actually vulnerable, but marked as resolved via cve_context, thus it must return 'false' here - .body("vulnerable", hasItems(true, false)) - .body("vulnStatus", hasItems("Analyzed", "Modified")); + .andExpect(jsonPath("$[*].vulnerable", hasItems(true, false))) + .andExpect(jsonPath("$[*].vulnStatus", hasItems("Analyzed", "Modified"))) + .andDo(doc("getKernelCvesForGardenLinuxByPackageName")); } @Test - public void shouldReturnKernelCvesForGardenLinuxByPackageName() { - given(this.spec).accept("application/json") - .when().port(this.port).get("/v1/cves/1592.6/packages/linux") - .then().statusCode(HttpStatus.SC_OK) - .body("cveId", hasItems("CVE-2025-21864", "CVE-2024-44953")) + void shouldReturnKernelCvesForGardenLinuxByPackageName() throws Exception { + this.mockMvc.perform(get("/v1/cves/1592.6/packages/linux")) + .andExpect(status().isOk()) // CVE-2024-44953 is vulnerable and not triaged for Garden Linux 1592.6 - .body("vulnerable", hasItems(true, true)); + .andExpect(jsonPath("$[*].cveId", hasItems("CVE-2025-21864", "CVE-2024-44953"))) + .andExpect(jsonPath("$[*].vulnerable", hasItems(true, true))) + .andDo(doc("getKernelCvesForGardenLinuxByPackageName")); } @Test - public void shouldReturnCvesForListOfPackages() { - given(this.spec).accept("application/json") - .filter(document("getCveForPackages", - preprocessRequest(modifyUris().scheme("https").host("security.gardenlinux.org").removePort()), - preprocessResponse(prettyPrint()))) - .when().port(this.port).get("/v1/cves/1592.4/packages/jinja2,vim") - .then().statusCode(HttpStatus.SC_OK); + void shouldReturnCvesForListOfPackages() throws Exception { + this.mockMvc.perform(get("/v1/cves/1592.4/packages/jinja2,vim")) + .andExpect(status().isOk()) + .andDo(doc("getCveForPackages")); } @Test - public void shouldReturnCvesForPutListOfPackages() { - var packageList = """ + void shouldReturnCvesForPutListOfPackages() throws Exception { + String packageList = """ { - "packageNames": [ - "vim", - "bash", - "python3", - "curl", - "jinja2" - ] - }"""; - - given(this.spec).accept("application/json") - .filter(document("getCveForPackagesPut", - preprocessRequest(modifyUris().scheme("https").host("security.gardenlinux.org").removePort()), - preprocessResponse(prettyPrint()))) - .contentType("application/json") - .body(packageList) - .when().port(this.port).put("/v1/cves/1592.4/packages") - .then().statusCode(HttpStatus.SC_OK); - } - - @Test - public void reproduceIssue167WithPut() { - var packageList = """ + "packageNames": [ + "vim", + "bash", + "python3", + "curl", + "jinja2" + ] + } + """; + this.mockMvc.perform( + put("/v1/cves/1592.4/packages") + .contentType("application/json") + .content(packageList) + ) + .andExpect(status().isOk()) + .andDo(doc("getCveForPackagesPut")); + } + + @Test + void reproduceIssue167WithPut() throws Exception { + String packageList = """ { - "packageNames": [ - "libselinux", - "util-linux" - ] - }"""; - - given(this.spec).accept("application/json") - .contentType("application/json") - .body(packageList) - .when().port(this.port).put("/v1/cves/1592.4/packages") - .then().statusCode(HttpStatus.SC_OK) - .body("size()", is(1)) - .body("sourcePackageName", hasItems("util-linux")) - .body("cveId", hasItems("CVE-2022-0563")); - } - - @Test - public void reproduceIssue167WithGet() { - given(this.spec).accept("application/json") - .when().port(this.port).get("/v1/cves/1592.4/packages/util-linux,libselinux") - .then().statusCode(HttpStatus.SC_OK) - .body("size()", is(1)) - .body("sourcePackageName", hasItems("util-linux")) - .body("cveId", hasItems("CVE-2022-0563")); + "packageNames": [ + "libselinux", + "util-linux" + ] + } + """; + this.mockMvc.perform( + put("/v1/cves/1592.4/packages") + .contentType("application/json") + .content(packageList) + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", hasSize(1))) + .andExpect(jsonPath("$[*].sourcePackageName", hasItem("util-linux"))) + .andExpect(jsonPath("$[*].cveId", hasItem("CVE-2022-0563"))); } @Test - public void shouldGetPackagesForDistro() { - given(this.spec).accept("application/json") - .filter(document("getPackages", - preprocessRequest(modifyUris().scheme("https").host("security.gardenlinux.org").removePort()), - preprocessResponse(prettyPrint()))) - .when().port(this.port).get("/v1/distro/1592.4") - .then().statusCode(200) - .body("sourcePackageName", hasItems("bind9", "dnsmasq", "jinja2", "systemd", "unbound")); - } - - @Test - public void shouldPackageWithVulnerabilities() { - given(this.spec).accept("application/json") - .filter(document("getPackageWithVulnerabilities", - preprocessRequest(modifyUris().scheme("https").host("security.gardenlinux.org").removePort()), - preprocessResponse(prettyPrint()))) - .when().port(this.port).get("/v1/packages/jinja2") - .then().statusCode(200) - .body("sourcePackageName", hasItems("jinja2")) - .body("cveId", hasItems("CVE-2024-56326")); - } - - @Test - public void shouldPackageWithVulnerabilitiesByVersion() { - given(this.spec).accept("application/json") - .filter(document("getPackageWithVulnerabilitiesByVersion", - preprocessRequest(modifyUris().scheme("https").host("security.gardenlinux.org").removePort()), - preprocessResponse(prettyPrint()))) - .when().port(this.port).get("/v1/packages/jinja2/3.1.3-1") - .then().statusCode(200) - .body("sourcePackageName", hasItems("jinja2")); - } - - @Test - public void shouldGetPackagesByVulnerability() { - given(this.spec).accept("application/json") - .filter(document("getPackagesByVulnerability", - preprocessRequest(modifyUris().scheme("https").host("security.gardenlinux.org").removePort()), - preprocessResponse(prettyPrint()))) - .when().port(this.port).get("/v1/distro/1592.4/CVE-2024-56326") - .then().statusCode(200); - } - - @Test - public void shouldGetCveDetailsWithContexts() { - given(this.spec).accept("application/json") - .filter(document("getCveDetailsWithContexts", - preprocessRequest(modifyUris().scheme("https").host("security.gardenlinux.org").removePort()), - preprocessResponse(prettyPrint()))) - .when().port(this.port).get("/v1/cveDetails/CVE-2023-50387") - .then().statusCode(200) - .body("details.cveId", equalTo("CVE-2023-50387")) - .body("contexts.description", hasItems("automated dummy data")); - } - - @Test - public void shouldGetCveDetailsWithMultipleContexts() { - given(this.spec).accept("application/json") - .filter(document("getCveDetailsWithMultipleContexts", - preprocessRequest(modifyUris().scheme("https").host("security.gardenlinux.org").removePort()), - preprocessResponse(prettyPrint()))) - .when().port(this.port).get("/v1/cveDetails/CVE-2024-21626") - .then().statusCode(200) - .body("details.cveId", equalTo("CVE-2024-21626")) - .body("contexts.description", hasItems("foo", "bar")); - } - - @Test - public void shouldGetCveDetailsWithContextsForKernelCve() { - given(this.spec).accept("application/json") - .filter(document("getCveDetailsWithContextsKernel", - preprocessRequest(modifyUris().scheme("https").host("security.gardenlinux.org").removePort()), - preprocessResponse(prettyPrint()))) - .when().port(this.port).get("/v1/cveDetails/CVE-2025-21864") - .then().statusCode(200) - .body("details.cveId", equalTo("CVE-2025-21864")) - .body("details.kernelLtsVersion[0]", equalTo("6.6")) - .body("details.kernelLtsVersion[1]", equalTo("6.12")) - .body("details.isVulnerable", is(List.of(false, true, true, true, false, false, false, false))) - .body("contexts.description", hasItems("Unit test for https://github.com/gardenlinux/glvd/issues/122")); - } - - @Test - public void shouldGetCveDetailsForKernelCve() { - given(this.spec).accept("application/json") - .filter(document("getCveDetailsKernel", - preprocessRequest(modifyUris().scheme("https").host("security.gardenlinux.org").removePort()), - preprocessResponse(prettyPrint()))) - .when().port(this.port).get("/v1/cveDetails/CVE-2024-53140") - .then().statusCode(200) - .body("details.cveId", equalTo("CVE-2024-53140")) - .body("details.kernelLtsVersion[0]", equalTo("6.6")) - .body("details.kernelLtsVersion[1]", equalTo("6.12")) - .body("details.isVulnerable", is(List.of(false, false, false, false, false, false, false, false))) - // has no cve context, assert it has no items in that list - .body("contexts", empty()); - } + void reproduceIssue167WithGet() throws Exception { + this.mockMvc.perform(get("/v1/cves/1592.4/packages/util-linux,libselinux")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", hasSize(1))) + .andExpect(jsonPath("$[*].sourcePackageName", hasItem("util-linux"))) + .andExpect(jsonPath("$[*].cveId", hasItem("CVE-2022-0563"))); + } - @Test - public void shouldGetCveDetailsWithContextsForKernelCveIsResolved() { - given(this.spec).accept("application/json") - .filter(document("getCveDetailsKernel", - preprocessRequest(modifyUris().scheme("https").host("security.gardenlinux.org").removePort()), - preprocessResponse(prettyPrint()))) - .when().port(this.port).get("/v1/cveDetails/CVE-2024-44953") - .then().statusCode(200) - .body("details.cveId", equalTo("CVE-2024-44953")) - .body("details.kernelLtsVersion[0]", equalTo("6.6")) - .body("details.kernelLtsVersion[1]", equalTo("6.12")) - // This CVE is not fixed in any kernel, so all are vulnerable - .body("details.isVulnerable", is(List.of(true, true, true, true, true, true, true))) - // Is explicitly marked as "resolved" - .body("contexts.resolved", hasItems(true)); - } - - @Test - public void shouldGetCveDetailsForNonDebianCVE() { - given(this.spec).accept("application/json") - .filter(document("getCveDetailsNonDebian", - preprocessRequest(modifyUris().scheme("https").host("security.gardenlinux.org").removePort()), - preprocessResponse(prettyPrint()))) - .when().port(this.port).get("/v1/cveDetails/CVE-2024-7344") - .then().statusCode(200) - .body("details.cveId", equalTo("CVE-2024-7344")) - .body("details.baseScoreV31", equalTo(8.2f)) - .body("details.vectorStringV31", equalTo("CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:C/C:H/I:H/A:H")); + @Test + void shouldGetPackagesForDistro() throws Exception { + this.mockMvc.perform(get("/v1/distro/1592.4")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[*].sourcePackageName", hasItems("bind9", "dnsmasq", "jinja2", "systemd", "unbound"))) + .andDo(doc("getPackages")); + } + + @Test + void shouldPackageWithVulnerabilities() throws Exception { + this.mockMvc.perform(get("/v1/packages/jinja2")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[*].sourcePackageName", hasItem("jinja2"))) + .andExpect(jsonPath("$[*].cveId", hasItem("CVE-2024-56326"))) + .andDo(doc("getPackageWithVulnerabilities")); + } + + @Test + void shouldPackageWithVulnerabilitiesByVersion() throws Exception { + this.mockMvc.perform(get("/v1/packages/jinja2/3.1.3-1")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[*].sourcePackageName", hasItem("jinja2"))) + .andDo(doc("getPackageWithVulnerabilitiesByVersion")); + } + + @Test + void shouldGetPackagesByVulnerability() throws Exception { + this.mockMvc.perform(get("/v1/distro/1592.4/CVE-2024-56326")) + .andExpect(status().isOk()) + .andDo(doc("getPackagesByVulnerability")); + } + + @Test + void shouldGetCveDetailsWithContexts() throws Exception { + this.mockMvc.perform(get("/v1/cveDetails/CVE-2023-50387")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.details.cveId", is("CVE-2023-50387"))) + .andExpect(jsonPath("$.contexts[0].description", is("automated dummy data"))) + .andDo(doc("getCveDetailsWithContexts")); + } + + @Test + void shouldGetCveDetailsWithMultipleContexts() throws Exception { + this.mockMvc.perform(get("/v1/cveDetails/CVE-2024-21626")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.details.cveId", is("CVE-2024-21626"))) + .andExpect(jsonPath("$.contexts[0].description", is("foo"))) + .andExpect(jsonPath("$.contexts[1].description", is("bar"))) + .andDo(doc("getCveDetailsWithMultipleContexts")); + } + + @Test + void shouldGetCveDetailsWithContextsForKernelCve() throws Exception { + this.mockMvc.perform(get("/v1/cveDetails/CVE-2025-21864")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.details.cveId", is("CVE-2025-21864"))) + .andExpect(jsonPath("$.details.kernelLtsVersion[0]", is("6.6"))) + .andExpect(jsonPath("$.details.kernelLtsVersion[1]", is("6.12"))) + .andExpect(jsonPath("$.details.isVulnerable", contains(false, true, true, true, false, false, false, false))) + .andExpect(jsonPath("$.contexts[0].description", is("Unit test for https://github.com/gardenlinux/glvd/issues/122"))) + .andDo(doc("getCveDetailsWithContextsKernel")); + } + + @Test + void shouldGetCveDetailsForKernelCve() throws Exception { + this.mockMvc.perform(get("/v1/cveDetails/CVE-2024-53140")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.details.cveId", is("CVE-2024-53140"))) + .andExpect(jsonPath("$.details.kernelLtsVersion[0]", is("6.6"))) + .andExpect(jsonPath("$.details.kernelLtsVersion[1]", is("6.12"))) + .andExpect(jsonPath("$.details.isVulnerable", contains(false, false, false, false, false, false, false, false))) + .andExpect(jsonPath("$.contexts", empty())) + .andDo(doc("getCveDetailsKernel")); + } + + @Test + void shouldGetCveDetailsWithContextsForKernelCveIsResolved() throws Exception { + this.mockMvc.perform(get("/v1/cveDetails/CVE-2024-44953")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.details.cveId", is("CVE-2024-44953"))) + .andExpect(jsonPath("$.details.kernelLtsVersion[0]", is("6.6"))) + .andExpect(jsonPath("$.details.kernelLtsVersion[1]", is("6.12"))) + .andExpect(jsonPath("$.details.isVulnerable", contains(true, true, true, true, true, true, true))) + .andExpect(jsonPath("$.contexts[0].resolved", is(true))) + .andDo(doc("getCveDetailsKernel")); + } + + @Test + void shouldGetCveDetailsForNonDebianCVE() throws Exception { + this.mockMvc.perform(get("/v1/cveDetails/CVE-2024-7344")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.details.cveId", is("CVE-2024-7344"))) + .andExpect(jsonPath("$.details.baseScoreV31", is(8.2))) + .andExpect(jsonPath("$.details.vectorStringV31", is("CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:C/C:H/I:H/A:H"))) + .andDo(doc("getCveDetailsNonDebian")); } @ParameterizedTest @ValueSource(strings = {"CVE-2024-7344", "CVE-2025-1419", "CVE-2004-0005", "CVE-2000-0258", "CVE-2000-0502", "CVE-2024-53564"}) - public void shouldGetCveDetailsForNonDebianCVEWithVariousCVESamples(String cveId) { - given(this.spec).accept("application/json") - .when().port(this.port).get("/v1/cveDetails/" + cveId) - .then().statusCode(200) - .body("details.cveId", equalTo(cveId)); + void shouldGetCveDetailsForNonDebianCVEWithVariousCVESamples(String cveId) throws Exception { + this.mockMvc.perform(get("/v1/cveDetails/" + cveId)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.details.cveId", is(cveId))); } @Test - public void shouldGetCveDetailsForNonAvailableCveIdGracefully() { - given(this.spec).accept("application/json") - .filter(document("getCveDetailsNonDebian", - preprocessRequest(modifyUris().scheme("https").host("security.gardenlinux.org").removePort()), - preprocessResponse(prettyPrint()))) - .when().port(this.port).get("/v1/cveDetails/INVALID_CVE_ID") - .then().statusCode(404) - .header("Message", "INVALID_CVE_ID is not in the GLVD database. It might either be very new and not yet be available in GLVD, or the ID might be misspelled."); + void shouldGetCveDetailsForNonAvailableCveIdGracefully() throws Exception { + this.mockMvc.perform(get("/v1/cveDetails/INVALID_CVE_ID")) + .andExpect(status().isNotFound()) + .andExpect(header().string("Message", containsString("INVALID_CVE_ID is not in the GLVD database. It might either be very new and not yet be available in GLVD, or the ID might be misspelled."))) + .andDo(doc("getCveDetailsNonDebian")); } @Test - public void shouldGenerateReleaseNotesInformationWithThreeDigitVersion() { - given(this.spec).accept("application/json") - .filter(document("releaseNotes", - preprocessRequest(modifyUris().scheme("https").host("security.gardenlinux.org").removePort()), - preprocessResponse(prettyPrint()))) - .when().port(this.port).get("/v1/releaseNotes/2000.1.0") - .then().statusCode(200) - .body("version", equalTo("2000.1.0")) - .body("packageList.sourcePackageName", hasItems("util-linux")) - .body("packageList.fixedCves", hasItems(List.of("CVE-2022-0563"))); + void shouldGenerateReleaseNotesInformationWithThreeDigitVersion() throws Exception { + this.mockMvc.perform(get("/v1/releaseNotes/2000.1.0")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.version", is("2000.1.0"))) + .andExpect(jsonPath("$.packageList[0].sourcePackageName", is("util-linux"))) + .andExpect(jsonPath("$.packageList[0].fixedCves", hasItems("CVE-2022-0563"))) + .andDo(doc("releaseNotes")); } @ParameterizedTest @ValueSource(strings = {"2000.1.0", "1592.5", "1592.7", "1592.8", "1443.20"}) - public void shouldGeneratePatchReleaseNotesInformationAcceptedVersionNumbers(String version) { - given(this.spec).accept("application/json") - .when().port(this.port).get("/v1/releaseNotes/" + version) - .then().statusCode(200) - .body("version", equalTo(version)); + void shouldGeneratePatchReleaseNotesInformationAcceptedVersionNumbers(String version) throws Exception { + this.mockMvc.perform(get("/v1/releaseNotes/" + version)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.version", is(version))); } @ParameterizedTest @ValueSource(strings = {"2001", "1.2.3.4", "v0.0.1", "1592.11-mycoolfork", "2002-11", "trixie", "today"}) - public void shouldFailWithProperErrorMessageWhenInvalidVersionSchemaIsSpecified(String version) { - given(this.spec).accept("application/json") - .when().port(this.port).get("/v1/releaseNotes/" + version) - .then().statusCode(400) - .header("Message", "gardenlinuxVersion must be in n.n or n.n.n format, but was: " + version); + void shouldFailWithProperErrorMessageWhenInvalidVersionSchemaIsSpecified(String version) throws Exception { + this.mockMvc.perform(get("/v1/releaseNotes/" + version)) + .andExpect(status().isBadRequest()) + .andExpect(header().string("Message", containsString("gardenlinuxVersion must be in n.n or n.n.n format, but was: " + version))); } @Test - public void shouldGeneratePatchReleaseNotesInformation() { - given(this.spec).accept("application/json") - .filter(document("patchReleaseNotes", - preprocessRequest(modifyUris().scheme("https").host("security.gardenlinux.org").removePort()), - preprocessResponse(prettyPrint()))) - .when().port(this.port).get("/v1/patchReleaseNotes/1592.5") - .then().statusCode(200) - .body("version", equalTo("1592.5")) - .body("packageList.sourcePackageName", hasItems("jinja2", "rsync", "curl", "python3.12")) - .body("packageList.fixedCves", hasItems(List.of("CVE-2024-56326"), List.of("CVE-2024-12085", - "CVE-2024-12086"), List.of("CVE-2024-11053"), List.of("CVE-2024-9287", "CVE-2025-0938"))); + void shouldGeneratePatchReleaseNotesInformation() throws Exception { + this.mockMvc.perform(get("/v1/patchReleaseNotes/1592.5")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.version", is("1592.5"))) + .andExpect(jsonPath("$.packageList[0].sourcePackageName", is(oneOf("jinja2", "rsync", "curl", "python3.12")))) + .andExpect(jsonPath("$.packageList[*].fixedCves[*]", hasItems( + "CVE-2024-56326", + "CVE-2024-12085", + "CVE-2024-12086", + "CVE-2024-11053", + "CVE-2024-9287", + "CVE-2025-0938" + ))) + .andDo(doc("patchReleaseNotes")); } @Test - public void shouldGeneratePatchReleaseNotesInformationWithKernelCveResolved() { - given(this.spec).accept("application/json") - .filter(document("patchReleaseNotesResolved", - preprocessRequest(modifyUris().scheme("https").host("security.gardenlinux.org").removePort()), - preprocessResponse(prettyPrint()))) - .when().port(this.port).get("/v1/patchReleaseNotes/1592.7") - .then().statusCode(200) - .body("version", equalTo("1592.7")) - .body("packageList.sourcePackageName", hasItems("linux")) - .body("packageList.fixedCves", hasItems(List.of("CVE-2025-21864"))); + void shouldGeneratePatchReleaseNotesInformationWithKernelCveResolved() throws Exception { + this.mockMvc.perform(get("/v1/patchReleaseNotes/1592.7")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.version", is("1592.7"))) + .andExpect(jsonPath("$.packageList[0].sourcePackageName", is("linux"))) + .andExpect(jsonPath("$.packageList[0].fixedCves", contains("CVE-2025-21864"))) + .andDo(doc("patchReleaseNotesResolved")); } @Test - public void shouldGenerateEmptyPatchReleaseNotesForDistWithNoSourcePackages() { - given(this.spec).accept("application/json") - .filter(document("patchReleaseNotesEmpty", - preprocessRequest(modifyUris().scheme("https").host("security.gardenlinux.org").removePort()), - preprocessResponse(prettyPrint()))) - .when().port(this.port).get("/v1/patchReleaseNotes/1592.8") - .then().statusCode(200) - .body("version", equalTo("1592.8")) - .body("packageList", empty()); + void shouldGenerateEmptyPatchReleaseNotesForDistWithNoSourcePackages() throws Exception { + this.mockMvc.perform(get("/v1/patchReleaseNotes/1592.8")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.version", is("1592.8"))) + .andExpect(jsonPath("$.packageList", empty())) + .andDo(doc("patchReleaseNotesEmpty")); } @Test - public void reproduceIssue153() { - // Reproducer for https://github.com/gardenlinux/glvd/issues/153 - given(this.spec).accept("application/json") - .when().port(this.port).get("/v1/patchReleaseNotes/1443.20") - .then().statusCode(200) - .body("version", equalTo("1443.20")) - .body("packageList", empty()); + void reproduceIssue153() throws Exception { + this.mockMvc.perform(get("/v1/patchReleaseNotes/1443.20")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.version", is("1443.20"))) + .andExpect(jsonPath("$.packageList", empty())); } @Test - public void shouldReturnAllTriages() { - given(this.spec).accept("application/json") - .filter(document("triagesList", - preprocessRequest(modifyUris().scheme("https").host("security.gardenlinux.org").removePort()), - preprocessResponse(prettyPrint()))) - .when().port(this.port).get("/v1/triage") - .then().statusCode(200); + void shouldReturnAllTriages() throws Exception { + this.mockMvc.perform(get("/v1/triage")) + .andExpect(status().isOk()) + .andDo(doc("triagesList")); } @Test - public void shouldReportExpectedTriagesForGardenlinuxVersion() { - given(this.spec).accept("application/json") - .filter(document("triagesGardenlinux", - preprocessRequest(modifyUris().scheme("https").host("security.gardenlinux.org").removePort()), - preprocessResponse(prettyPrint()))) - .when().port(this.port).get("/v1/triage/gardenlinux/1592.9") - .then().statusCode(200) - .body("cveId", hasItems("CVE-2005-2541", "CVE-2019-1010022")); + void shouldReportExpectedTriagesForGardenlinuxVersion() throws Exception { + this.mockMvc.perform(get("/v1/triage/gardenlinux/1592.9")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[*].cveId", hasItems("CVE-2005-2541", "CVE-2019-1010022"))) + .andDo(doc("triagesGardenlinux")); } @Test - public void shouldReportExpectedTriagesForCveId() { - given(this.spec).accept("application/json") - .filter(document("triagesCve", - preprocessRequest(modifyUris().scheme("https").host("security.gardenlinux.org").removePort()), - preprocessResponse(prettyPrint()))) - .when().port(this.port).get("/v1/triage/cve/CVE-2019-1010022") - .then().statusCode(200) - .body("cveId", hasItems("CVE-2019-1010022")); + void shouldReportExpectedTriagesForCveId() throws Exception { + this.mockMvc.perform(get("/v1/triage/cve/CVE-2019-1010022")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[*].cveId", hasItem("CVE-2019-1010022"))) + .andDo(doc("triagesCve")); } @Test - public void shouldReportExpectedTriagesForPackage() { - given(this.spec).accept("application/json") - .filter(document("triagesPackage", - preprocessRequest(modifyUris().scheme("https").host("security.gardenlinux.org").removePort()), - preprocessResponse(prettyPrint()))) - .when().port(this.port).get("/v1/triage/sourcePackage/glibc") - .then().statusCode(200) - .body("cveId", hasItems("CVE-2019-1010022")); + void shouldReportExpectedTriagesForPackage() throws Exception { + this.mockMvc.perform(get("/v1/triage/sourcePackage/glibc")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[*].cveId", hasItem("CVE-2019-1010022"))) + .andDo(doc("triagesPackage")); } @Test - public void shouldReportNoTriagesForGardenlinuxVersionWithoutCveContexts() { - given(this.spec).accept("application/json") - .filter(document("triagesEmpty", - preprocessRequest(modifyUris().scheme("https").host("security.gardenlinux.org").removePort()), - preprocessResponse(prettyPrint()))) - .when().port(this.port).get("/v1/triage/gardenlinux/1592.8") - .then().statusCode(200) - .body("", empty()); + void shouldReportNoTriagesForGardenlinuxVersionWithoutCveContexts() throws Exception { + this.mockMvc.perform(get("/v1/triage/gardenlinux/1592.8")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", empty())) + .andDo(doc("triagesEmpty")); } @Test - public void shouldResolveGardenLinuxVersionToDistId() { - given(this.spec).accept("text/plain") - .when().port(this.port).get("/v1/distro/1592.10/distId") - .then().statusCode(200) - .body(equalTo("24")); + void shouldResolveGardenLinuxVersionToDistId() throws Exception { + this.mockMvc.perform(get("/v1/distro/1592.10/distId").accept("text/plain")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", is(24))); } @Test - public void shouldGetAllGardenLinuxVersions() { - given(this.spec).accept("application/json") - .filter(document("getAllGardenLinuxVersions", - preprocessRequest(modifyUris().scheme("https").host("security.gardenlinux.org").removePort()), - preprocessResponse(prettyPrint()))) - .when().port(this.port).get("/v1/gardenlinuxVersions") - .then().statusCode(200) - .body("$", hasItems("1592.4", "1592.5", "1592.6", "1592.7", "1592.8", "1592.9", "1592.10", "today")); + void shouldGetAllGardenLinuxVersions() throws Exception { + this.mockMvc.perform(get("/v1/gardenlinuxVersions")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", hasItems("1592.4", "1592.5", "1592.6", "1592.7", "1592.8", "1592.9", "1592.10", "today"))) + .andDo(doc("getAllGardenLinuxVersions")); } - }