From 2f7489ae4f474b8f70a6c0cc8fc1bd810fe870cf Mon Sep 17 00:00:00 2001 From: Florian Wilhelm <2292245+fwilhe@users.noreply.github.com> Date: Tue, 7 May 2024 10:49:38 +0200 Subject: [PATCH] Implement endpoint to get CVEs per distro (#5) --- build.gradle | 14 ++++- src/docs/asciidoc/index.adoc | 27 ++++++++ .../io/gardenlinux/glvd/CveRepository.java | 7 --- .../io/gardenlinux/glvd/GlvdController.java | 14 ++++- .../java/io/gardenlinux/glvd/GlvdService.java | 17 ++++- .../glvd/{Cve.java => db/CveEntity.java} | 12 ++-- .../io/gardenlinux/glvd/db/CveRepository.java | 25 ++++++++ .../java/io/gardenlinux/glvd/dto/Cve.java | 53 ++++++++++++++++ .../glvd/exceptions/NotFoundException.java | 8 +++ src/main/resources/application.properties | 13 +--- .../gardenlinux/glvd/GlvdControllerTest.java | 62 ++++++++++++++++--- 11 files changed, 210 insertions(+), 42 deletions(-) create mode 100644 src/docs/asciidoc/index.adoc delete mode 100644 src/main/java/io/gardenlinux/glvd/CveRepository.java rename src/main/java/io/gardenlinux/glvd/{Cve.java => db/CveEntity.java} (77%) create mode 100644 src/main/java/io/gardenlinux/glvd/db/CveRepository.java create mode 100644 src/main/java/io/gardenlinux/glvd/dto/Cve.java create mode 100644 src/main/java/io/gardenlinux/glvd/exceptions/NotFoundException.java diff --git a/build.gradle b/build.gradle index 1d6b656..b4c72ba 100644 --- a/build.gradle +++ b/build.gradle @@ -20,7 +20,12 @@ 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-web' @@ -28,7 +33,7 @@ dependencies { runtimeOnly 'org.postgresql:postgresql' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.boot:spring-boot-testcontainers' - testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc' + testImplementation 'org.springframework.restdocs:spring-restdocs-restassured' testImplementation 'org.testcontainers:junit-jupiter' testImplementation 'org.testcontainers:postgresql' testImplementation 'io.rest-assured:rest-assured:5.4.0' @@ -40,6 +45,13 @@ tasks.named('test') { } tasks.named('asciidoctor') { + configurations "asciidoctorExtensions" inputs.dir snippetsDir dependsOn test } +bootJar { + dependsOn asciidoctor + from ("${asciidoctor.outputDir}/html5") { + into 'static/docs' + } +} \ No newline at end of file diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc new file mode 100644 index 0000000..4f7550e --- /dev/null +++ b/src/docs/asciidoc/index.adoc @@ -0,0 +1,27 @@ += Garden Linux Vulnerability Database Rest API +Garden Linux Authors; +:doctype: book +:icons: font +:source-highlighter: highlightjs + +== Lorem Ipsum + +=== Get a CVE by id + +Request + +include::{snippets}/getCve/curl-request.adoc[] + +Response + +include::{snippets}/getCve/http-response.adoc[] + +=== Get a list of CVEs by distro + +Request + +include::{snippets}/getCveForDistro/curl-request.adoc[] + +Response + +include::{snippets}/getCveForDistro/http-response.adoc[] \ No newline at end of file diff --git a/src/main/java/io/gardenlinux/glvd/CveRepository.java b/src/main/java/io/gardenlinux/glvd/CveRepository.java deleted file mode 100644 index 57296c5..0000000 --- a/src/main/java/io/gardenlinux/glvd/CveRepository.java +++ /dev/null @@ -1,7 +0,0 @@ -package io.gardenlinux.glvd; - -import org.springframework.data.jpa.repository.JpaRepository; - -public interface CveRepository extends JpaRepository { - -} diff --git a/src/main/java/io/gardenlinux/glvd/GlvdController.java b/src/main/java/io/gardenlinux/glvd/GlvdController.java index fea6cc6..25056f6 100644 --- a/src/main/java/io/gardenlinux/glvd/GlvdController.java +++ b/src/main/java/io/gardenlinux/glvd/GlvdController.java @@ -1,5 +1,7 @@ package io.gardenlinux.glvd; +import io.gardenlinux.glvd.dto.Cve; +import io.gardenlinux.glvd.exceptions.NotFoundException; import jakarta.annotation.Nonnull; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; @@ -8,6 +10,8 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import java.util.List; + @RestController @RequestMapping(value = "/v1/cves", produces = MediaType.APPLICATION_JSON_VALUE) public class GlvdController { @@ -20,9 +24,13 @@ public GlvdController(@Nonnull GlvdService glvdService) { } @GetMapping("/{cveId}") - ResponseEntity getCveId(@PathVariable("cveId") final String cveId) { - var cve = glvdService.getCve(cveId); - return ResponseEntity.ok().body(cve); + ResponseEntity getCveId(@PathVariable("cveId") final String cveId) throws NotFoundException { + return ResponseEntity.ok().body(glvdService.getCve(cveId)); + } + + @GetMapping("/{vendor}/{product}/{codename}") + ResponseEntity> getCveDistro(@PathVariable final String vendor, @PathVariable final String product, @PathVariable final String codename) { + return ResponseEntity.ok().body(glvdService.getCveForDistribution(vendor, product, codename)); } } diff --git a/src/main/java/io/gardenlinux/glvd/GlvdService.java b/src/main/java/io/gardenlinux/glvd/GlvdService.java index ef84f8c..3485627 100644 --- a/src/main/java/io/gardenlinux/glvd/GlvdService.java +++ b/src/main/java/io/gardenlinux/glvd/GlvdService.java @@ -1,8 +1,13 @@ package io.gardenlinux.glvd; +import io.gardenlinux.glvd.db.CveRepository; +import io.gardenlinux.glvd.dto.Cve; +import io.gardenlinux.glvd.exceptions.NotFoundException; import jakarta.annotation.Nonnull; import org.springframework.stereotype.Service; +import java.util.List; + @Service public class GlvdService { @@ -13,8 +18,14 @@ public GlvdService(@Nonnull CveRepository cveRepository) { this.cveRepository = cveRepository; } - public Cve getCve(String cveId) { - var cve = cveRepository.findById(cveId); - return cve.orElseThrow(); + public Cve getCve(String cveId) throws NotFoundException { + var cveEntity = cveRepository.findById(cveId).orElseThrow(NotFoundException::new); + // Todo: more specific transformation from db type 'cve' to response type 'cve' + return new Cve(cveEntity.getId(), cveEntity.getLastModified(), cveEntity.getData()); + + } + + public List getCveForDistribution(String vendor, String product, String codename) { + return cveRepository.cvesForDistribution(vendor, product, codename); } } diff --git a/src/main/java/io/gardenlinux/glvd/Cve.java b/src/main/java/io/gardenlinux/glvd/db/CveEntity.java similarity index 77% rename from src/main/java/io/gardenlinux/glvd/Cve.java rename to src/main/java/io/gardenlinux/glvd/db/CveEntity.java index 3686a27..7d4d63c 100644 --- a/src/main/java/io/gardenlinux/glvd/Cve.java +++ b/src/main/java/io/gardenlinux/glvd/db/CveEntity.java @@ -1,4 +1,4 @@ -package io.gardenlinux.glvd; +package io.gardenlinux.glvd.db; import jakarta.annotation.Nonnull; import jakarta.persistence.Column; @@ -8,7 +8,7 @@ import java.util.Objects; @Entity(name = "all_cve") -public class Cve { +public class CveEntity { @Id @Column(name = "cve_id", nullable = false) private String id; @@ -21,10 +21,10 @@ public class Cve { @Nonnull private String data; - public Cve() { + public CveEntity() { } - public Cve(String id, @Nonnull String lastModified, @Nonnull String data) { + public CveEntity(String id, @Nonnull String lastModified, @Nonnull String data) { this.id = id; this.lastModified = lastModified; this.data = data; @@ -49,8 +49,8 @@ public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; - Cve cve = (Cve) o; - return Objects.equals(id, cve.id) && lastModified.equals(cve.lastModified) && data.equals(cve.data); + CveEntity cveEntity = (CveEntity) o; + return Objects.equals(id, cveEntity.id) && lastModified.equals(cveEntity.lastModified) && data.equals(cveEntity.data); } @Override diff --git a/src/main/java/io/gardenlinux/glvd/db/CveRepository.java b/src/main/java/io/gardenlinux/glvd/db/CveRepository.java new file mode 100644 index 0000000..2967510 --- /dev/null +++ b/src/main/java/io/gardenlinux/glvd/db/CveRepository.java @@ -0,0 +1,25 @@ +package io.gardenlinux.glvd.db; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +import java.util.List; + +public interface CveRepository extends JpaRepository { + + @Query(value = """ + SELECT + all_cve.data AS cveEntity + FROM + all_cve + INNER JOIN deb_cve USING (cve_id) + INNER JOIN dist_cpe ON (deb_cve.dist_id = dist_cpe.id) + WHERE + dist_cpe.cpe_vendor = ?1 AND + dist_cpe.cpe_product = ?2 and + dist_cpe.deb_codename = ?3 + ORDER BY + all_cve.cve_id + """, nativeQuery = true) + List cvesForDistribution(String vendor, String product, String codename); +} diff --git a/src/main/java/io/gardenlinux/glvd/dto/Cve.java b/src/main/java/io/gardenlinux/glvd/dto/Cve.java new file mode 100644 index 0000000..b6d95ac --- /dev/null +++ b/src/main/java/io/gardenlinux/glvd/dto/Cve.java @@ -0,0 +1,53 @@ +package io.gardenlinux.glvd.dto; + +import jakarta.annotation.Nonnull; + +import java.util.Objects; + +public class Cve { + private String id; + @Nonnull + private String lastModified; + @Nonnull + private String data; + + public Cve() { + } + + public Cve(String id, @Nonnull String lastModified, @Nonnull String data) { + this.id = id; + this.lastModified = lastModified; + this.data = data; + } + + public String getId() { + return id; + } + + @Nonnull + public String getLastModified() { + return lastModified; + } + + @Nonnull + public String getData() { + return data; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Cve Cve = (Cve) o; + return Objects.equals(id, Cve.id) && lastModified.equals(Cve.lastModified) && data.equals(Cve.data); + } + + @Override + public int hashCode() { + int result = Objects.hashCode(id); + result = 31 * result + lastModified.hashCode(); + result = 31 * result + data.hashCode(); + return result; + } +} diff --git a/src/main/java/io/gardenlinux/glvd/exceptions/NotFoundException.java b/src/main/java/io/gardenlinux/glvd/exceptions/NotFoundException.java new file mode 100644 index 0000000..8eb2c80 --- /dev/null +++ b/src/main/java/io/gardenlinux/glvd/exceptions/NotFoundException.java @@ -0,0 +1,8 @@ +package io.gardenlinux.glvd.exceptions; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(code = HttpStatus.NOT_FOUND, reason = "Not Found") +public class NotFoundException extends Exception{ +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index d19bb43..de878ed 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -2,15 +2,4 @@ spring.application.name=glvd spring.datasource.url=jdbc:postgresql://localhost:5432/glvd spring.datasource.username=glvd spring.datasource.password=glvd -spring.sql.init.mode=always - -logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE - - -spring.jpa.properties.hibernate.show_sql=true -spring.jpa.properties.hibernate.use_sql_comments=true -spring.jpa.properties.hibernate.format_sql=true -logging.level.org.hibernate.type=trace - - -logging.level.org.hibernate.orm.jdbc.bind=trace \ No newline at end of file +spring.sql.init.mode=never diff --git a/src/test/java/io/gardenlinux/glvd/GlvdControllerTest.java b/src/test/java/io/gardenlinux/glvd/GlvdControllerTest.java index a7efbad..15ef0ef 100644 --- a/src/test/java/io/gardenlinux/glvd/GlvdControllerTest.java +++ b/src/test/java/io/gardenlinux/glvd/GlvdControllerTest.java @@ -1,27 +1,41 @@ package io.gardenlinux.glvd; +import io.gardenlinux.glvd.db.CveRepository; import io.restassured.RestAssured; +import io.restassured.builder.RequestSpecBuilder; import io.restassured.http.ContentType; +import io.restassured.specification.RequestSpecification; +import org.apache.http.HttpStatus; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; 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.boot.test.web.server.LocalServerPort; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; import org.springframework.test.context.DynamicPropertyRegistry; import org.springframework.test.context.DynamicPropertySource; +import org.springframework.test.context.junit.jupiter.SpringExtension; import org.testcontainers.containers.PostgreSQLContainer; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; import org.testcontainers.utility.DockerImageName; import static io.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.Matchers.containsString; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.modifyUris; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @Testcontainers +@ExtendWith({RestDocumentationExtension.class, SpringExtension.class}) class GlvdControllerTest { static DockerImageName glvdPostgresImage = DockerImageName.parse("ghcr.io/gardenlinux/glvd-postgres:edgesampledata") @@ -38,6 +52,16 @@ 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; + } + @BeforeAll static void beforeAll() { postgres.start(); @@ -55,19 +79,20 @@ static void configureProperties(DynamicPropertyRegistry registry) { registry.add("spring.datasource.password", postgres::getPassword); } - @BeforeEach - void setUp() { - RestAssured.baseURI = "http://localhost:" + port; - } - @Test - void shouldGetCveById() { - given() - .contentType(ContentType.JSON) + public void shouldGetCveById() { + given(this.spec) + .accept("application/json") + .filter(document("getCve", + preprocessRequest(modifyUris() + .scheme("https") + .host("glvd.gardenlinux.io") + .removePort()))) .when() + .port(this.port) .get("/v1/cves/CVE-2024-1549") .then() - .statusCode(200) + .statusCode(HttpStatus.SC_OK) .body("id", containsString("CVE-2024-1549")); } @@ -78,6 +103,23 @@ void tryGetNonExistingCveById() { .when() .get("/v1/cves/CVE-1989-1234") .then() - .statusCode(500); + .statusCode(HttpStatus.SC_NOT_FOUND); } + + @Test + public void shouldReturnCvesForBookworm() { + given(this.spec) + .accept("application/json") + .filter(document("getCveForDistro", + preprocessRequest(modifyUris() + .scheme("https") + .host("glvd.gardenlinux.io") + .removePort()))) + .when() + .port(this.port) + .get("/v1/cves/debian/debian_linux/bookworm") + .then() + .statusCode(HttpStatus.SC_OK); + } + } \ No newline at end of file