From 08c2a2ebff8ea4d7094c55cc0c7594568b9000d2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Nov 2025 02:11:21 +0000 Subject: [PATCH 01/17] Bump org.springframework.boot from 3.5.7 to 4.0.0 Bumps [org.springframework.boot](https://github.com/spring-projects/spring-boot) from 3.5.7 to 4.0.0. - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.7...v4.0.0) --- updated-dependencies: - dependency-name: org.springframework.boot dependency-version: 4.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 27f464e..677d626 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.0' id 'io.spring.dependency-management' version '1.1.7' id 'org.asciidoctor.jvm.convert' version '4.0.5' } From 17002303b93f29e66883d76a4c2f9c23c7a06008 Mon Sep 17 00:00:00 2001 From: Florian Wilhelm Date: Mon, 24 Nov 2025 14:55:21 +0100 Subject: [PATCH 02/17] wip --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 677d626..ffbb240 100644 --- a/build.gradle +++ b/build.gradle @@ -34,7 +34,7 @@ dependencies { implementation 'org.commonmark:commonmark:0.27.0' runtimeOnly 'org.postgresql:postgresql' testImplementation 'org.springframework.boot:spring-boot-starter-test' - testImplementation 'org.springframework.restdocs:spring-restdocs-restassured' + testImplementation 'org.springframework.boot:spring-boot-restdocs' testImplementation 'io.rest-assured:rest-assured:5.5.6' testImplementation 'org.junit.jupiter:junit-jupiter:6.0.1' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' From a5bc999dfbf25cd225ee396e162cf42a6b5c4a45 Mon Sep 17 00:00:00 2001 From: Florian Wilhelm Date: Mon, 5 Jan 2026 09:23:56 +0100 Subject: [PATCH 03/17] WIP upgrade to spring boot 4 need to address restdocs tests later --- build.gradle | 20 +- .../glvd/db/NvdCveDataAttributeConverter.java | 10 +- src/main/resources/application.properties | 3 +- .../glvd/ActuatorEndpointTests.java | 53 -- .../gardenlinux/glvd/GlvdControllerTest.java | 474 ------------------ 5 files changed, 16 insertions(+), 544 deletions(-) delete mode 100644 src/test/java/io/gardenlinux/glvd/ActuatorEndpointTests.java delete mode 100644 src/test/java/io/gardenlinux/glvd/GlvdControllerTest.java diff --git a/build.gradle b/build.gradle index ffbb240..f8a4e3b 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'org.springframework.boot' version '4.0.0' + 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' } @@ -20,23 +20,21 @@ ext { set('snippetsDir', file("build/generated-snippets")) } -configurations { - asciidoctorExtensions -} - 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-webmvc' + + implementation 'org.springframework.boot:spring-boot-starter-actuator' 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-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.boot:spring-boot-restdocs' - 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' } 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 deleted file mode 100644 index ca74cd8..0000000 --- a/src/test/java/io/gardenlinux/glvd/ActuatorEndpointTests.java +++ /dev/null @@ -1,53 +0,0 @@ -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.boot.test.context.SpringBootTest; -import org.springframework.boot.test.web.server.LocalServerPort; -import org.springframework.test.context.junit.jupiter.SpringExtension; - -import static io.restassured.RestAssured.given; -import static org.hamcrest.Matchers.is; - -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -@ExtendWith({SpringExtension.class}) -public class ActuatorEndpointTests { - - @LocalServerPort - private Integer port; - - private RequestSpecification spec; - - @BeforeEach - void setUp() { - this.spec = new RequestSpecBuilder().build(); - - RestAssured.baseURI = "http://localhost:" + port; - } - - // 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")); - } - - @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)); - } - -} diff --git a/src/test/java/io/gardenlinux/glvd/GlvdControllerTest.java b/src/test/java/io/gardenlinux/glvd/GlvdControllerTest.java deleted file mode 100644 index 6584bfc..0000000 --- a/src/test/java/io/gardenlinux/glvd/GlvdControllerTest.java +++ /dev/null @@ -1,474 +0,0 @@ -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.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 java.util.List; - -import static io.restassured.RestAssured.given; -import static org.hamcrest.Matchers.*; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; -import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; -import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; - -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -@ExtendWith({RestDocumentationExtension.class, SpringExtension.class}) -class GlvdControllerTest { - - @LocalServerPort - private Integer port; - - private RequestSpecification spec; - - @BeforeEach - void setUp(RestDocumentationContextProvider restDocumentation) { - this.spec = new RequestSpecBuilder().addFilter(documentationConfiguration(restDocumentation)).build(); - - RestAssured.baseURI = "http://localhost:" + port; - } - - @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")); - } - - @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")); - } - - @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")) - // 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")); - } - - @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) - // 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")); - } - - @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")) - // 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")); - } - - @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")) - // CVE-2024-44953 is vulnerable and not triaged for Garden Linux 1592.6 - .body("vulnerable", hasItems(true, true)); - } - - @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); - } - - @Test - public void shouldReturnCvesForPutListOfPackages() { - var 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": [ - "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")); - } - - @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()); - } - - @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")); - } - - @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)); - } - - @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."); - } - - @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"))); - } - - @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)); - } - - @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); - } - - @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"))); - } - - @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"))); - } - - @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()); - } - - @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()); - } - - @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); - } - - @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")); - } - - @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")); - } - - @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")); - } - - @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()); - } - - @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")); - } - - @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")); - } - -} From da94b4ab99e83dd45aa9f493f47698151aef4178 Mon Sep 17 00:00:00 2001 From: Florian Wilhelm Date: Mon, 5 Jan 2026 10:25:26 +0100 Subject: [PATCH 04/17] asciidoctorExtensions --- build.gradle | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/build.gradle b/build.gradle index f8a4e3b..0cc9ed2 100644 --- a/build.gradle +++ b/build.gradle @@ -20,7 +20,13 @@ ext { set('snippetsDir', file("build/generated-snippets")) } +configurations { + asciidoctorExtensions +} + dependencies { + asciidoctorExtensions 'org.springframework.restdocs:spring-restdocs-asciidoctor' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-webmvc' From 1f3e6ba556fb7c558d946105d795d18a4a920da7 Mon Sep 17 00:00:00 2001 From: Florian Wilhelm Date: Mon, 5 Jan 2026 10:38:49 +0100 Subject: [PATCH 05/17] use org.asciidoctor:asciidoctorj:3.0.1 --- build.gradle | 2 ++ 1 file changed, 2 insertions(+) diff --git a/build.gradle b/build.gradle index 0cc9ed2..1bc9a42 100644 --- a/build.gradle +++ b/build.gradle @@ -35,6 +35,8 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' implementation 'org.commonmark:commonmark:0.27.0' implementation 'org.apache.commons:commons-lang3:3.20.0' + implementation 'org.asciidoctor:asciidoctorj:3.0.1' + runtimeOnly 'org.postgresql:postgresql' testImplementation 'org.springframework.boot:spring-boot-restdocs' From a8434eeb24cf04a918b19cb413d2aea0fc0a0e52 Mon Sep 17 00:00:00 2001 From: Florian Wilhelm Date: Mon, 5 Jan 2026 10:44:10 +0100 Subject: [PATCH 06/17] try w/o snippets for now --- src/docs/asciidoc/index.adoc | 208 ----------------------------------- 1 file changed, 208 deletions(-) diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc index b673d30..092d482 100644 --- a/src/docs/asciidoc/index.adoc +++ b/src/docs/asciidoc/index.adoc @@ -11,211 +11,3 @@ Find out more about GLVD at https://security.gardenlinux.org and https://github. This document provides real HTTP requests and responses captured from API tests. The data shown is based on unit tests and may differ from production data, but the structure of requests and responses remains consistent. - -== Triage Data - -Triage is the process where the Garden Linux security team evaluates security vulnerabilities (CVEs) to determine their impact on Garden Linux releases. -Getting Triage data is one of the main features of the GLVD API. - -[TIP] -.Understanding the 'resolved' Field -==== -In GLVD, triage data gives extra context about a CVE for a specific Garden Linux release. For example, it can mark a CVE as a false positive or note that a fix is available upstream and will be included in a future release. Note that there may be multiple triages over time for the same CVE on the same Garden Linux version, as we go through the lifecycle of responding to the CVE. - -The `resolved` (or `triageMarkedAsResolved`) field acts as an override. If set to `true`, it tells GLVD to treat the vulnerability as resolved, regardless of other data. If `false`, it has no effect. - -The main field to check is `vulnerable`, which is available in the 'CVE Data' endpoints listed in this document. Its value depends on several factors, including data from the Debian Security Tracker and the `resolved` field. - -Triages only exist for CVEs that were reported as 'vulnerable' by our upstream data sources. -==== - -=== List Triages for a Garden Linux Release - -Retrieve triaged security vulnerabilities for a Garden Linux release. - -include::{snippets}/triagesGardenlinux/curl-request.adoc[] - -Example response: - -include::{snippets}/triagesGardenlinux/http-response.adoc[] - -=== Get Triages for a CVE - -Retrieve triage information for a specific CVE by its ID. - -include::{snippets}/triagesCve/curl-request.adoc[] - -Example response: - -include::{snippets}/triagesCve/http-response.adoc[] - -=== Get Triages for a Debian Source Package - -Retrieve triage information for all CVEs related to a Debian source package. - -include::{snippets}/triagesPackage/curl-request.adoc[] - -Example response: - -include::{snippets}/triagesPackage/http-response.adoc[] - -=== List All Triages - -Retrieve a list of triages regardless of the Garden Linux release. - -include::{snippets}/triagesList/curl-request.adoc[] - -Example response: - -include::{snippets}/triagesList/http-response.adoc[] - -== CVE Data - -=== List CVEs by Distribution - -Retrieve all CVEs for a given distribution and version: - -include::{snippets}/getCveForDistro/curl-request.adoc[] - -TIP: The `sortBy` and `sortOrder` query parameters are optional. If omitted, default sorting is applied. - -Example response: - -include::{snippets}/getCveForDistro/http-response.adoc[] - -=== List CVEs by Image - -Retrieve all CVEs for a given Garden Linux image and version. -This applies a filter for the packages in the specified image. - -Supported images are currently: - -- `ali-gardener_prod` -- `aws-gardener_prod` -- `azure-gardener_prod` -- `gcp-gardener_prod` -- `openstack-gardener_prod` - -include::{snippets}/getCveForImage/curl-request.adoc[] - -Example response: - -include::{snippets}/getCveForImage/http-response.adoc[] - -=== List CVEs for Packages by Distribution - -Retrieve all CVEs for a list of packages in a specified distribution. -Package names are comma-separated (URL-encoding may be required). - -include::{snippets}/getCveForPackages/curl-request.adoc[] - -Example response: - -include::{snippets}/getCveForPackages/http-response.adoc[] - -=== List CVEs for Packages by Distribution (PUT) - -Retrieve all CVEs for a list of packages in a specified distribution. -Package names are provided in the request body as JSON. - -include::{snippets}/getCveForPackagesPut/curl-request.adoc[] - -Example response: - -include::{snippets}/getCveForPackagesPut/http-response.adoc[] - -=== Get Vulnerabilities for a Package - -Retrieve vulnerabilities for a specific package. - -include::{snippets}/getPackageWithVulnerabilities/curl-request.adoc[] - -Example response: - -include::{snippets}/getPackageWithVulnerabilities/http-response.adoc[] - -=== Get Vulnerabilities for a Package by Version - -Retrieve vulnerabilities for a specific package and version. - -include::{snippets}/getPackageWithVulnerabilitiesByVersion/curl-request.adoc[] - -Example response: - -include::{snippets}/getPackageWithVulnerabilitiesByVersion/http-response.adoc[] - -=== List Packages Affected by a Vulnerability - -Retrieve a list of packages affected by a specific vulnerability. - -include::{snippets}/getPackagesByVulnerability/curl-request.adoc[] - -Example response: - -include::{snippets}/getPackagesByVulnerability/http-response.adoc[] - -=== Get CVE Details with Triage Data - -Retrieve information about a CVE by its ID. -If triage data is available for this CVE, it is included in the response. - -include::{snippets}/getCveDetailsWithContexts/curl-request.adoc[] - -Example response: - -include::{snippets}/getCveDetailsWithContexts/http-response.adoc[] - -==== Linux Kernel CVEs - -For CVEs affecting the Linux kernel, the response structure differs. -Data is sourced from upstream kernel developers for LTS kernel versions in Garden Linux. - -NOTE: For the Linux kernel, Garden Linux always builds LTS kernels directly from the upstream source, applying both Debian patches and custom patches as needed. Therefore, CVEs reported by Debian for the kernel do not directly apply. Instead, kernel vulnerability information is sourced from upstream kernel developers, and Debian kernel CVEs are ignored in Garden Linux. - -include::{snippets}/getCveDetailsWithContextsKernel/curl-request.adoc[] - -Example response: - -include::{snippets}/getCveDetailsWithContextsKernel/http-response.adoc[] - -=== Get CVE Details for Non-Debian CVEs - -For CVEs not present in the Debian Security Tracker, only basic information is provided. - -include::{snippets}/getCveDetailsNonDebian/curl-request.adoc[] - -Example response: - -include::{snippets}/getCveDetailsNonDebian/http-response.adoc[] - -== Garden Linux Release Data - -=== List All Garden Linux Releases - -Retrieve all known Garden Linux releases in GLVD. - -include::{snippets}/getAllGardenLinuxVersions/curl-request.adoc[] - -Example response: - -include::{snippets}/getAllGardenLinuxVersions/http-response.adoc[] - -=== List Packages in a Distribution - -Retrieve a list of packages for a given distribution. - -include::{snippets}/getPackages/curl-request.adoc[] - -Example response: - -include::{snippets}/getPackages/http-response.adoc[] - -=== Get Release Notes - -Retrieve information about fixed security vulnerabilities in a minor release of Garden Linux. - -include::{snippets}/releaseNotes/curl-request.adoc[] - -Example response: - -include::{snippets}/releaseNotes/http-response.adoc[] From 5e8d460c2d8b716505f253da4e14f5e84596a999 Mon Sep 17 00:00:00 2001 From: Florian Wilhelm Date: Mon, 5 Jan 2026 10:50:13 +0100 Subject: [PATCH 07/17] try org.jruby:jruby:10.0.2.0 --- build.gradle | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build.gradle b/build.gradle index 1bc9a42..2fbbdbb 100644 --- a/build.gradle +++ b/build.gradle @@ -39,6 +39,9 @@ dependencies { runtimeOnly 'org.postgresql:postgresql' + testImplementation("org.jruby:jruby:10.0.2.0") + + testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.boot:spring-boot-restdocs' testImplementation 'org.springframework.boot:spring-boot-starter-data-jpa-test' testImplementation 'org.springframework.boot:spring-boot-starter-webmvc-test' From 1a099a9351f74806211b9d2c1f93ea896df60094 Mon Sep 17 00:00:00 2001 From: Florian Wilhelm Date: Mon, 5 Jan 2026 11:10:06 +0100 Subject: [PATCH 08/17] minimal api test sample --- build.gradle | 18 +++---- src/docs/asciidoc/index.adoc | 4 ++ .../glvd/ApiDocumentationTest.java | 48 +++++++++++++++++++ 3 files changed, 62 insertions(+), 8 deletions(-) create mode 100644 src/test/java/io/gardenlinux/glvd/ApiDocumentationTest.java diff --git a/build.gradle b/build.gradle index 2fbbdbb..91b2779 100644 --- a/build.gradle +++ b/build.gradle @@ -24,6 +24,10 @@ configurations { asciidoctorExtensions } +asciidoctorj { + version = "3.0.0" +} + dependencies { asciidoctorExtensions 'org.springframework.restdocs:spring-restdocs-asciidoctor' @@ -35,33 +39,31 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' implementation 'org.commonmark:commonmark:0.27.0' implementation 'org.apache.commons:commons-lang3:3.20.0' - implementation 'org.asciidoctor:asciidoctorj:3.0.1' runtimeOnly 'org.postgresql:postgresql' - testImplementation("org.jruby:jruby:10.0.2.0") testImplementation 'org.springframework.boot:spring-boot-starter-test' - testImplementation 'org.springframework.boot:spring-boot-restdocs' 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 +tasks.named("test") { useJUnitPlatform() + outputs.dir snippetsDir } -tasks.named('asciidoctor') { +tasks.named("asciidoctor") { 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/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc index 092d482..6dc767d 100644 --- a/src/docs/asciidoc/index.adoc +++ b/src/docs/asciidoc/index.adoc @@ -11,3 +11,7 @@ Find out more about GLVD at https://security.gardenlinux.org and https://github. This document provides real HTTP requests and responses captured from API tests. The data shown is based on unit tests and may differ from production data, but the structure of requests and responses remains consistent. + +include::{snippets}/index-example/curl-request.md[] + + diff --git a/src/test/java/io/gardenlinux/glvd/ApiDocumentationTest.java b/src/test/java/io/gardenlinux/glvd/ApiDocumentationTest.java new file mode 100644 index 0000000..ca045f1 --- /dev/null +++ b/src/test/java/io/gardenlinux/glvd/ApiDocumentationTest.java @@ -0,0 +1,48 @@ +package io.gardenlinux.glvd; + +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.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; +import org.springframework.restdocs.templates.TemplateFormats; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import tools.jackson.databind.json.JsonMapper; + +@SpringBootTest(properties = "debug=true") +@ExtendWith(RestDocumentationExtension.class) +class ApiDocumentationTest { + + @Autowired + private JsonMapper jsonMapper; + + @Autowired + private WebApplicationContext context; + + private MockMvc mockMvc; + + @BeforeEach + public void setUp(RestDocumentationContextProvider restDocumentation) { + this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(documentationConfiguration(restDocumentation).snippets().withTemplateFormat(TemplateFormats.markdown())).build(); + } + + + @Test + void indexExample() throws Exception { + this.mockMvc.perform(get("/")) + .andExpect(status().isOk()) + .andDo(document("index-example")); + + } +} From 1888e0cf6fefd1b4e3507af674e68bdec4d7caf0 Mon Sep 17 00:00:00 2001 From: Florian Wilhelm Date: Mon, 5 Jan 2026 12:25:35 +0100 Subject: [PATCH 09/17] actuator tests --- src/docs/asciidoc/index.adoc | 2 +- .../glvd/ActuatorEndpointTests.java | 50 +++++++++++++++++++ .../glvd/ApiDocumentationTest.java | 2 +- 3 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 src/test/java/io/gardenlinux/glvd/ActuatorEndpointTests.java diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc index 6dc767d..75d6e85 100644 --- a/src/docs/asciidoc/index.adoc +++ b/src/docs/asciidoc/index.adoc @@ -12,6 +12,6 @@ Find out more about GLVD at https://security.gardenlinux.org and https://github. This document provides real HTTP requests and responses captured from API tests. The data shown is based on unit tests and may differ from production data, but the structure of requests and responses remains consistent. -include::{snippets}/index-example/curl-request.md[] +include::{snippets}/index-example/curl-request.adoc[] diff --git a/src/test/java/io/gardenlinux/glvd/ActuatorEndpointTests.java b/src/test/java/io/gardenlinux/glvd/ActuatorEndpointTests.java new file mode 100644 index 0000000..9a9f7af --- /dev/null +++ b/src/test/java/io/gardenlinux/glvd/ActuatorEndpointTests.java @@ -0,0 +1,50 @@ +package io.gardenlinux.glvd; + +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.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +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 +@ExtendWith(RestDocumentationExtension.class) +class ActuatorEndpointTests { + + @Autowired + private WebApplicationContext context; + + private MockMvc mockMvc; + + @BeforeEach + public void setUp() { + this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context).build(); + } + + // We want to use actuator for k8s liveness and readiness probes + @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) throws Exception { + this.mockMvc.perform(get("/actuator/" + endpoint)) + .andExpect(status().isNotFound()); + } + +} diff --git a/src/test/java/io/gardenlinux/glvd/ApiDocumentationTest.java b/src/test/java/io/gardenlinux/glvd/ApiDocumentationTest.java index ca045f1..b49f644 100644 --- a/src/test/java/io/gardenlinux/glvd/ApiDocumentationTest.java +++ b/src/test/java/io/gardenlinux/glvd/ApiDocumentationTest.java @@ -34,7 +34,7 @@ class ApiDocumentationTest { @BeforeEach public void setUp(RestDocumentationContextProvider restDocumentation) { this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(documentationConfiguration(restDocumentation).snippets().withTemplateFormat(TemplateFormats.markdown())).build(); + .apply(documentationConfiguration(restDocumentation).snippets()).build(); } From 6ec36f1c568e1d63d61cb1ec704517f1ef2234a9 Mon Sep 17 00:00:00 2001 From: Florian Wilhelm Date: Mon, 5 Jan 2026 13:11:22 +0100 Subject: [PATCH 10/17] start converting tests --- src/docs/asciidoc/index.adoc | 2 +- .../glvd/ApiDocumentationTest.java | 48 ------------ .../gardenlinux/glvd/GlvdControllerTest.java | 76 +++++++++++++++++++ 3 files changed, 77 insertions(+), 49 deletions(-) delete mode 100644 src/test/java/io/gardenlinux/glvd/ApiDocumentationTest.java create mode 100644 src/test/java/io/gardenlinux/glvd/GlvdControllerTest.java diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc index 75d6e85..2e3bc17 100644 --- a/src/docs/asciidoc/index.adoc +++ b/src/docs/asciidoc/index.adoc @@ -12,6 +12,6 @@ Find out more about GLVD at https://security.gardenlinux.org and https://github. This document provides real HTTP requests and responses captured from API tests. The data shown is based on unit tests and may differ from production data, but the structure of requests and responses remains consistent. -include::{snippets}/index-example/curl-request.adoc[] +include::{snippets}/getCveForImage/curl-request.adoc[] diff --git a/src/test/java/io/gardenlinux/glvd/ApiDocumentationTest.java b/src/test/java/io/gardenlinux/glvd/ApiDocumentationTest.java deleted file mode 100644 index b49f644..0000000 --- a/src/test/java/io/gardenlinux/glvd/ApiDocumentationTest.java +++ /dev/null @@ -1,48 +0,0 @@ -package io.gardenlinux.glvd; - -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.test.web.servlet.result.MockMvcResultMatchers.status; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.restdocs.RestDocumentationContextProvider; -import org.springframework.restdocs.RestDocumentationExtension; -import org.springframework.restdocs.templates.TemplateFormats; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.web.context.WebApplicationContext; - -import tools.jackson.databind.json.JsonMapper; - -@SpringBootTest(properties = "debug=true") -@ExtendWith(RestDocumentationExtension.class) -class ApiDocumentationTest { - - @Autowired - private JsonMapper jsonMapper; - - @Autowired - private WebApplicationContext context; - - private MockMvc mockMvc; - - @BeforeEach - public void setUp(RestDocumentationContextProvider restDocumentation) { - this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(documentationConfiguration(restDocumentation).snippets()).build(); - } - - - @Test - void indexExample() throws Exception { - this.mockMvc.perform(get("/")) - .andExpect(status().isOk()) - .andDo(document("index-example")); - - } -} diff --git a/src/test/java/io/gardenlinux/glvd/GlvdControllerTest.java b/src/test/java/io/gardenlinux/glvd/GlvdControllerTest.java new file mode 100644 index 0000000..13a2188 --- /dev/null +++ b/src/test/java/io/gardenlinux/glvd/GlvdControllerTest.java @@ -0,0 +1,76 @@ +package io.gardenlinux.glvd; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; +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 tools.jackson.databind.json.JsonMapper; + +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.operation.preprocess.Preprocessors.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@ExtendWith(RestDocumentationExtension.class) +class GlvdControllerTest { + + @Autowired + private JsonMapper jsonMapper; + + @Autowired + private WebApplicationContext context; + + private MockMvc mockMvc; + + static ResultHandler doc(String identifier) { + return document( + identifier, + preprocessRequest(modifyUris().host("security.gardenlinux.org").removePort()), + preprocessResponse(prettyPrint()) + ); + } + + @BeforeEach + public void setUp(RestDocumentationContextProvider restDocumentation) { + this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(documentationConfiguration(restDocumentation).snippets().withAdditionalDefaults()).build(); + } + + @Test + 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 + 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 + void shouldReturnCvesForGardenlinuxShouldNotReturnKernelCveMarkedAsResolved() throws Exception { + var foo = 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 + .andExpect(jsonPath("$[*].vulnerable", contains(false, true, false))) + .andExpect(jsonPath("$[*].vulnStatus", contains("Received", "Analyzed", "Modified"))); + } +} From 28e5ab887cfa2c3bbd170659192901bdbf02748c Mon Sep 17 00:00:00 2001 From: Florian Wilhelm Date: Mon, 5 Jan 2026 13:24:38 +0100 Subject: [PATCH 11/17] refactor: remove unnecessary variable assignment in CVE test --- src/test/java/io/gardenlinux/glvd/GlvdControllerTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/io/gardenlinux/glvd/GlvdControllerTest.java b/src/test/java/io/gardenlinux/glvd/GlvdControllerTest.java index 13a2188..3315391 100644 --- a/src/test/java/io/gardenlinux/glvd/GlvdControllerTest.java +++ b/src/test/java/io/gardenlinux/glvd/GlvdControllerTest.java @@ -65,7 +65,7 @@ void shouldReturnCvesForGardenlinux() throws Exception { @Test void shouldReturnCvesForGardenlinuxShouldNotReturnKernelCveMarkedAsResolved() throws Exception { - var foo = this.mockMvc.perform(get("/v1/cves/1592.5?sortBy=cveId&sortOrder=DESC")) + 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"))) From b7da98300b69bf780584a1d8b696ef9cead54b29 Mon Sep 17 00:00:00 2001 From: Florian Wilhelm Date: Mon, 5 Jan 2026 17:12:04 +0100 Subject: [PATCH 12/17] port tests to mockmvc --- src/docs/asciidoc/index.adoc | 204 +++++++++++ .../gardenlinux/glvd/GlvdControllerTest.java | 338 +++++++++++++++++- 2 files changed, 535 insertions(+), 7 deletions(-) diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc index 2e3bc17..b673d30 100644 --- a/src/docs/asciidoc/index.adoc +++ b/src/docs/asciidoc/index.adoc @@ -12,6 +12,210 @@ Find out more about GLVD at https://security.gardenlinux.org and https://github. This document provides real HTTP requests and responses captured from API tests. The data shown is based on unit tests and may differ from production data, but the structure of requests and responses remains consistent. +== Triage Data + +Triage is the process where the Garden Linux security team evaluates security vulnerabilities (CVEs) to determine their impact on Garden Linux releases. +Getting Triage data is one of the main features of the GLVD API. + +[TIP] +.Understanding the 'resolved' Field +==== +In GLVD, triage data gives extra context about a CVE for a specific Garden Linux release. For example, it can mark a CVE as a false positive or note that a fix is available upstream and will be included in a future release. Note that there may be multiple triages over time for the same CVE on the same Garden Linux version, as we go through the lifecycle of responding to the CVE. + +The `resolved` (or `triageMarkedAsResolved`) field acts as an override. If set to `true`, it tells GLVD to treat the vulnerability as resolved, regardless of other data. If `false`, it has no effect. + +The main field to check is `vulnerable`, which is available in the 'CVE Data' endpoints listed in this document. Its value depends on several factors, including data from the Debian Security Tracker and the `resolved` field. + +Triages only exist for CVEs that were reported as 'vulnerable' by our upstream data sources. +==== + +=== List Triages for a Garden Linux Release + +Retrieve triaged security vulnerabilities for a Garden Linux release. + +include::{snippets}/triagesGardenlinux/curl-request.adoc[] + +Example response: + +include::{snippets}/triagesGardenlinux/http-response.adoc[] + +=== Get Triages for a CVE + +Retrieve triage information for a specific CVE by its ID. + +include::{snippets}/triagesCve/curl-request.adoc[] + +Example response: + +include::{snippets}/triagesCve/http-response.adoc[] + +=== Get Triages for a Debian Source Package + +Retrieve triage information for all CVEs related to a Debian source package. + +include::{snippets}/triagesPackage/curl-request.adoc[] + +Example response: + +include::{snippets}/triagesPackage/http-response.adoc[] + +=== List All Triages + +Retrieve a list of triages regardless of the Garden Linux release. + +include::{snippets}/triagesList/curl-request.adoc[] + +Example response: + +include::{snippets}/triagesList/http-response.adoc[] + +== CVE Data + +=== List CVEs by Distribution + +Retrieve all CVEs for a given distribution and version: + +include::{snippets}/getCveForDistro/curl-request.adoc[] + +TIP: The `sortBy` and `sortOrder` query parameters are optional. If omitted, default sorting is applied. + +Example response: + +include::{snippets}/getCveForDistro/http-response.adoc[] + +=== List CVEs by Image + +Retrieve all CVEs for a given Garden Linux image and version. +This applies a filter for the packages in the specified image. + +Supported images are currently: + +- `ali-gardener_prod` +- `aws-gardener_prod` +- `azure-gardener_prod` +- `gcp-gardener_prod` +- `openstack-gardener_prod` + include::{snippets}/getCveForImage/curl-request.adoc[] +Example response: + +include::{snippets}/getCveForImage/http-response.adoc[] + +=== List CVEs for Packages by Distribution + +Retrieve all CVEs for a list of packages in a specified distribution. +Package names are comma-separated (URL-encoding may be required). + +include::{snippets}/getCveForPackages/curl-request.adoc[] + +Example response: + +include::{snippets}/getCveForPackages/http-response.adoc[] + +=== List CVEs for Packages by Distribution (PUT) + +Retrieve all CVEs for a list of packages in a specified distribution. +Package names are provided in the request body as JSON. + +include::{snippets}/getCveForPackagesPut/curl-request.adoc[] + +Example response: + +include::{snippets}/getCveForPackagesPut/http-response.adoc[] + +=== Get Vulnerabilities for a Package + +Retrieve vulnerabilities for a specific package. + +include::{snippets}/getPackageWithVulnerabilities/curl-request.adoc[] + +Example response: + +include::{snippets}/getPackageWithVulnerabilities/http-response.adoc[] + +=== Get Vulnerabilities for a Package by Version + +Retrieve vulnerabilities for a specific package and version. + +include::{snippets}/getPackageWithVulnerabilitiesByVersion/curl-request.adoc[] + +Example response: + +include::{snippets}/getPackageWithVulnerabilitiesByVersion/http-response.adoc[] + +=== List Packages Affected by a Vulnerability + +Retrieve a list of packages affected by a specific vulnerability. + +include::{snippets}/getPackagesByVulnerability/curl-request.adoc[] + +Example response: + +include::{snippets}/getPackagesByVulnerability/http-response.adoc[] + +=== Get CVE Details with Triage Data + +Retrieve information about a CVE by its ID. +If triage data is available for this CVE, it is included in the response. + +include::{snippets}/getCveDetailsWithContexts/curl-request.adoc[] + +Example response: + +include::{snippets}/getCveDetailsWithContexts/http-response.adoc[] + +==== Linux Kernel CVEs + +For CVEs affecting the Linux kernel, the response structure differs. +Data is sourced from upstream kernel developers for LTS kernel versions in Garden Linux. + +NOTE: For the Linux kernel, Garden Linux always builds LTS kernels directly from the upstream source, applying both Debian patches and custom patches as needed. Therefore, CVEs reported by Debian for the kernel do not directly apply. Instead, kernel vulnerability information is sourced from upstream kernel developers, and Debian kernel CVEs are ignored in Garden Linux. + +include::{snippets}/getCveDetailsWithContextsKernel/curl-request.adoc[] + +Example response: + +include::{snippets}/getCveDetailsWithContextsKernel/http-response.adoc[] + +=== Get CVE Details for Non-Debian CVEs + +For CVEs not present in the Debian Security Tracker, only basic information is provided. + +include::{snippets}/getCveDetailsNonDebian/curl-request.adoc[] + +Example response: + +include::{snippets}/getCveDetailsNonDebian/http-response.adoc[] + +== Garden Linux Release Data + +=== List All Garden Linux Releases + +Retrieve all known Garden Linux releases in GLVD. + +include::{snippets}/getAllGardenLinuxVersions/curl-request.adoc[] + +Example response: + +include::{snippets}/getAllGardenLinuxVersions/http-response.adoc[] + +=== List Packages in a Distribution + +Retrieve a list of packages for a given distribution. + +include::{snippets}/getPackages/curl-request.adoc[] + +Example response: + +include::{snippets}/getPackages/http-response.adoc[] + +=== Get Release Notes + +Retrieve information about fixed security vulnerabilities in a minor release of Garden Linux. + +include::{snippets}/releaseNotes/curl-request.adoc[] + +Example response: +include::{snippets}/releaseNotes/http-response.adoc[] diff --git a/src/test/java/io/gardenlinux/glvd/GlvdControllerTest.java b/src/test/java/io/gardenlinux/glvd/GlvdControllerTest.java index 3315391..ebd94e4 100644 --- a/src/test/java/io/gardenlinux/glvd/GlvdControllerTest.java +++ b/src/test/java/io/gardenlinux/glvd/GlvdControllerTest.java @@ -3,6 +3,8 @@ 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.restdocs.RestDocumentationContextProvider; @@ -11,23 +13,18 @@ import org.springframework.test.web.servlet.ResultHandler; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; -import tools.jackson.databind.json.JsonMapper; 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.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @SpringBootTest @ExtendWith(RestDocumentationExtension.class) class GlvdControllerTest { - - @Autowired - private JsonMapper jsonMapper; - @Autowired private WebApplicationContext context; @@ -73,4 +70,331 @@ void shouldReturnCvesForGardenlinuxShouldNotReturnKernelCveMarkedAsResolved() th .andExpect(jsonPath("$[*].vulnerable", contains(false, true, false))) .andExpect(jsonPath("$[*].vulnStatus", contains("Received", "Analyzed", "Modified"))); } + + @Test + void shouldReturnCvesForGardenlinuxWithRejectedCve() throws Exception { + this.mockMvc.perform(get("/v1/cves/1592.7?sortBy=cveId&sortOrder=DESC")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", hasSize(3))) + .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 + 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"))) + .andExpect(jsonPath("$[*].vulnerable", hasItems(true, false))) + .andExpect(jsonPath("$[*].vulnStatus", hasItems("Analyzed", "Modified"))) + .andDo(doc("getKernelCvesForGardenLinuxByPackageName")); + } + + @Test + void shouldReturnKernelCvesForGardenLinuxByPackageName() throws Exception { + this.mockMvc.perform(get("/v1/cves/1592.6/packages/linux")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[*].cveId", hasItems("CVE-2025-21864", "CVE-2024-44953"))) + .andExpect(jsonPath("$[*].vulnerable", hasItems(true, true))) + .andDo(doc("getKernelCvesForGardenLinuxByPackageName")); + } + + @Test + void shouldReturnCvesForListOfPackages() throws Exception { + this.mockMvc.perform(get("/v1/cves/1592.4/packages/jinja2,vim")) + .andExpect(status().isOk()) + .andDo(doc("getCveForPackages")); + } + + @Test + void shouldReturnCvesForPutListOfPackages() throws Exception { + String 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" + ] + } + """; + 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 + 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 + 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"}) + void shouldGetCveDetailsForNonDebianCVEWithVariousCVESamples(String cveId) throws Exception { + this.mockMvc.perform(get("/v1/cveDetails/" + cveId)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.details.cveId", is(cveId))); + } + + @Test + 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 + 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"}) + 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"}) + 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 + 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 + 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 + 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 + 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 + void shouldReturnAllTriages() throws Exception { + this.mockMvc.perform(get("/v1/triage")) + .andExpect(status().isOk()) + .andDo(doc("triagesList")); + } + + @Test + 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 + 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 + 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 + void shouldReportNoTriagesForGardenlinuxVersionWithoutCveContexts() throws Exception { + this.mockMvc.perform(get("/v1/triage/gardenlinux/1592.8")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", empty())) + .andDo(doc("triagesEmpty")); + } + + @Test + void shouldResolveGardenLinuxVersionToDistId() throws Exception { + this.mockMvc.perform(get("/v1/distro/1592.10/distId").accept("text/plain")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", is(24))); + } + + @Test + 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")); + } } From ba7d7765160dbd4bd2f41ccc61356b86be72f3b6 Mon Sep 17 00:00:00 2001 From: Florian Wilhelm Date: Mon, 5 Jan 2026 17:25:39 +0100 Subject: [PATCH 13/17] style: unify string quotes and clean up build.gradle --- build.gradle | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/build.gradle b/build.gradle index 91b2779..97f92f9 100644 --- a/build.gradle +++ b/build.gradle @@ -17,7 +17,7 @@ repositories { } ext { - set('snippetsDir', file("build/generated-snippets")) + set('snippetsDir', file('build/generated-snippets')) } configurations { @@ -25,7 +25,7 @@ configurations { } asciidoctorj { - version = "3.0.0" + version = '3.0.0' } dependencies { @@ -33,16 +33,12 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-webmvc' - implementation 'org.springframework.boot:spring-boot-starter-actuator' implementation 'org.springframework.boot:spring-boot-starter-web' 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.boot:spring-boot-starter-data-jpa-test' testImplementation 'org.springframework.boot:spring-boot-starter-webmvc-test' @@ -50,18 +46,18 @@ dependencies { testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } -tasks.named("test") { +tasks.named('test') { useJUnitPlatform() outputs.dir snippetsDir } -tasks.named("asciidoctor") { - configurations "asciidoctorExtensions" +tasks.named('asciidoctor') { + configurations 'asciidoctorExtensions' inputs.dir snippetsDir dependsOn test } -tasks.named("bootJar") { +tasks.named('bootJar') { dependsOn asciidoctor from("${asciidoctor.outputDir}") { into 'static/docs' From 52bbb621b49669b07b3b13f414c7612d291d1ef2 Mon Sep 17 00:00:00 2001 From: Florian Wilhelm Date: Mon, 5 Jan 2026 17:40:38 +0100 Subject: [PATCH 14/17] test: add comments to clarify expected behavior of CVE tests --- src/test/java/io/gardenlinux/glvd/GlvdControllerTest.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/test/java/io/gardenlinux/glvd/GlvdControllerTest.java b/src/test/java/io/gardenlinux/glvd/GlvdControllerTest.java index ebd94e4..3906b3c 100644 --- a/src/test/java/io/gardenlinux/glvd/GlvdControllerTest.java +++ b/src/test/java/io/gardenlinux/glvd/GlvdControllerTest.java @@ -76,6 +76,7 @@ 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' .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))) @@ -88,6 +89,7 @@ void shouldReturnKernelCvesForGardenLinuxByPackageNameAndMarkResolvedCveAsNotVul 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 .andExpect(jsonPath("$[*].vulnerable", hasItems(true, false))) .andExpect(jsonPath("$[*].vulnStatus", hasItems("Analyzed", "Modified"))) .andDo(doc("getKernelCvesForGardenLinuxByPackageName")); @@ -97,6 +99,7 @@ void shouldReturnKernelCvesForGardenLinuxByPackageNameAndMarkResolvedCveAsNotVul 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 .andExpect(jsonPath("$[*].cveId", hasItems("CVE-2025-21864", "CVE-2024-44953"))) .andExpect(jsonPath("$[*].vulnerable", hasItems(true, true))) .andDo(doc("getKernelCvesForGardenLinuxByPackageName")); From 252d0981f9b14d0be68480a5fbfca59043ef5c0c Mon Sep 17 00:00:00 2001 From: Florian Wilhelm Date: Tue, 6 Jan 2026 09:57:30 +0100 Subject: [PATCH 15/17] update major versions in dependabot again --- .github/dependabot.yml | 3 --- 1 file changed, 3 deletions(-) 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: '/' From a4d7119d84cdcbcb5903dfe5aee5236987a88a9d Mon Sep 17 00:00:00 2001 From: Florian Wilhelm Date: Tue, 6 Jan 2026 09:59:46 +0100 Subject: [PATCH 16/17] reorder deps --- build.gradle | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index 97f92f9..cf2c0ea 100644 --- a/build.gradle +++ b/build.gradle @@ -30,11 +30,10 @@ asciidoctorj { dependencies { asciidoctorExtensions 'org.springframework.restdocs:spring-restdocs-asciidoctor' - - implementation 'org.springframework.boot:spring-boot-starter-data-jpa' - implementation 'org.springframework.boot:spring-boot-starter-webmvc' 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.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' From 7f47ad1b2ec961cc91174ca02a30eb3ec3269d35 Mon Sep 17 00:00:00 2001 From: Florian Wilhelm Date: Tue, 6 Jan 2026 10:11:37 +0100 Subject: [PATCH 17/17] improve test --- .../java/io/gardenlinux/glvd/ActuatorEndpointTests.java | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/test/java/io/gardenlinux/glvd/ActuatorEndpointTests.java b/src/test/java/io/gardenlinux/glvd/ActuatorEndpointTests.java index 9a9f7af..58c8817 100644 --- a/src/test/java/io/gardenlinux/glvd/ActuatorEndpointTests.java +++ b/src/test/java/io/gardenlinux/glvd/ActuatorEndpointTests.java @@ -2,13 +2,10 @@ 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.restdocs.RestDocumentationContextProvider; -import org.springframework.restdocs.RestDocumentationExtension; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; @@ -19,7 +16,6 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @SpringBootTest -@ExtendWith(RestDocumentationExtension.class) class ActuatorEndpointTests { @Autowired @@ -44,7 +40,8 @@ void shouldReturnHealth() throws Exception { @ValueSource(strings = {"prometheus", "metrics", "env", "heapdump", "beans", "loggers", "mappings", "shutdown"}) public void shouldNotReturnSensitiveEndpoints(String endpoint) throws Exception { this.mockMvc.perform(get("/actuator/" + endpoint)) - .andExpect(status().isNotFound()); + .andExpect(status().isNotFound()) + .andExpect(status().reason("No static resource actuator/" + endpoint + ".")); } }