diff --git a/api-examples/Get CVE by Id.bru b/api-examples/Get CVE by Id.bru new file mode 100644 index 0000000..b787357 --- /dev/null +++ b/api-examples/Get CVE by Id.bru @@ -0,0 +1,11 @@ +meta { + name: Get CVE by Id + type: http + seq: 2 +} + +get { + url: http://localhost:8080/v1/cves/CVE-2024-1547 + body: none + auth: none +} diff --git a/api-examples/Get CVEs by Distro Codename Packages.bru b/api-examples/Get CVEs by Distro Codename Packages.bru new file mode 100644 index 0000000..442dd37 --- /dev/null +++ b/api-examples/Get CVEs by Distro Codename Packages.bru @@ -0,0 +1,11 @@ +meta { + name: Get CVEs by Distro Codename Packages + type: http + seq: 4 +} + +get { + url: http://localhost:8080/v1/cves/debian_linux/bookworm/packages/vim,firefox-esr + body: none + auth: none +} diff --git a/api-examples/Get CVEs by Distro Codename.bru b/api-examples/Get CVEs by Distro Codename.bru new file mode 100644 index 0000000..f799afc --- /dev/null +++ b/api-examples/Get CVEs by Distro Codename.bru @@ -0,0 +1,11 @@ +meta { + name: Get CVEs by Distro Codename + type: http + seq: 3 +} + +get { + url: http://localhost:8080/v1/cves/debian_linux/bookworm + body: none + auth: none +} diff --git a/api-examples/Get CVEs by Distro Version Packages.bru b/api-examples/Get CVEs by Distro Version Packages.bru new file mode 100644 index 0000000..9cd9cc5 --- /dev/null +++ b/api-examples/Get CVEs by Distro Version Packages.bru @@ -0,0 +1,11 @@ +meta { + name: Get CVEs by Distro Version Packages + type: http + seq: 5 +} + +get { + url: http://localhost:8080/v1/cves/debian_linux/version/12/packages/vim,firefox-esr + body: none + auth: none +} diff --git a/api-examples/README.md b/api-examples/README.md new file mode 100644 index 0000000..b3b1f06 --- /dev/null +++ b/api-examples/README.md @@ -0,0 +1 @@ +Get https://www.usebruno.com to use those api example requests to play with the api. \ No newline at end of file diff --git a/api-examples/bruno.json b/api-examples/bruno.json new file mode 100644 index 0000000..dfe486d --- /dev/null +++ b/api-examples/bruno.json @@ -0,0 +1,9 @@ +{ + "version": "1", + "name": "glvd", + "type": "collection", + "ignore": [ + "node_modules", + ".git" + ] +} \ No newline at end of file diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc index 5d23fcf..1013831 100644 --- a/src/docs/asciidoc/index.adoc +++ b/src/docs/asciidoc/index.adoc @@ -32,10 +32,34 @@ include::{snippets}/getCve/http-response.adoc[] === Get a list of CVEs by distro -To query all CVEs for a given distribution, you may use this endpoint: +To query all CVEs for a given distribution by codename, you may use this endpoint: include::{snippets}/getCveForDistro/curl-request.adoc[] The expected response looks like this: -include::{snippets}/getCveForDistro/http-response.adoc[] \ No newline at end of file +include::{snippets}/getCveForDistro/http-response.adoc[] + +To use a specific distribution version number, use this endpoint: + +include::{snippets}/getCveForDistroByVersion/curl-request.adoc[] + +The expected response looks like this: + +include::{snippets}/getCveForDistroByVersion/http-response.adoc[] + +=== Get a list of CVEs for packages by distro + +include::{snippets}/getCveForPackages/curl-request.adoc[] + +The expected response looks like this: + +include::{snippets}/getCveForPackages/http-response.adoc[] + +To use a specific distribution version number, use this endpoint: + +include::{snippets}/getCveForPackagesByDistroVersion/curl-request.adoc[] + +The expected response looks like this: + +include::{snippets}/getCveForPackagesByDistroVersion/http-response.adoc[] \ No newline at end of file diff --git a/src/main/java/io/gardenlinux/glvd/GlvdController.java b/src/main/java/io/gardenlinux/glvd/GlvdController.java index 44778ea..4d4c22a 100644 --- a/src/main/java/io/gardenlinux/glvd/GlvdController.java +++ b/src/main/java/io/gardenlinux/glvd/GlvdController.java @@ -1,7 +1,7 @@ package io.gardenlinux.glvd; -import io.gardenlinux.glvd.dto.Cve; -import io.gardenlinux.glvd.exceptions.CantParseJSONException; +import io.gardenlinux.glvd.db.CveEntity; +import io.gardenlinux.glvd.db.SourcePackageCve; import io.gardenlinux.glvd.exceptions.NotFoundException; import jakarta.annotation.Nonnull; import org.springframework.http.MediaType; @@ -25,14 +25,36 @@ public GlvdController(@Nonnull GlvdService glvdService) { } @GetMapping("/{cveId}") - ResponseEntity getCveId(@PathVariable("cveId") final String cveId) throws NotFoundException { + 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) throws CantParseJSONException { - return ResponseEntity.ok().body(glvdService.getCveForDistribution(vendor, product, codename)); + @GetMapping("/{product}/{codename}") + ResponseEntity> getCveDistro(@PathVariable final String product, + @PathVariable final String codename) { + return ResponseEntity.ok().body(glvdService.getCveForDistribution(product, codename)); + } + + + @GetMapping("/{product}/version/{version}") + ResponseEntity> getCveDistroVersion(@PathVariable final String product, + @PathVariable final String version) { + return ResponseEntity.ok().body(glvdService.getCveForDistributionVersion(product, version)); + } + + + @GetMapping("/{product}/{codename}/packages/{packageList}") + ResponseEntity> getCvePackages(@PathVariable final String product, + @PathVariable final String codename, @PathVariable final String packageList) { + var cveForPackages = glvdService.getCveForPackages(product, codename, packageList); + return ResponseEntity.ok().body(cveForPackages); + } + + @GetMapping("/{product}/version/{version}/packages/{packageList}") + ResponseEntity> getCvePackagesVersion(@PathVariable final String product, + @PathVariable final String version, @PathVariable final String packageList) { + var cveForPackages = glvdService.getCveForPackagesVersion(product, version, packageList); + return ResponseEntity.ok().body(cveForPackages); } } diff --git a/src/main/java/io/gardenlinux/glvd/GlvdService.java b/src/main/java/io/gardenlinux/glvd/GlvdService.java index 8104b6d..febabe4 100644 --- a/src/main/java/io/gardenlinux/glvd/GlvdService.java +++ b/src/main/java/io/gardenlinux/glvd/GlvdService.java @@ -1,13 +1,10 @@ package io.gardenlinux.glvd; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; import io.gardenlinux.glvd.db.CveEntity; import io.gardenlinux.glvd.db.CveRepository; import io.gardenlinux.glvd.db.HealthCheckRepository; -import io.gardenlinux.glvd.dto.Cve; +import io.gardenlinux.glvd.db.SourcePackageCve; import io.gardenlinux.glvd.dto.Readiness; -import io.gardenlinux.glvd.exceptions.CantParseJSONException; import io.gardenlinux.glvd.exceptions.DbNotConnectedException; import io.gardenlinux.glvd.exceptions.NotFoundException; import jakarta.annotation.Nonnull; @@ -24,12 +21,9 @@ public class GlvdService { @Nonnull private final HealthCheckRepository healthCheckRepository; - private final ObjectMapper objectMapper; - public GlvdService(@Nonnull CveRepository cveRepository, @Nonnull HealthCheckRepository healthCheckRepository) { this.cveRepository = cveRepository; this.healthCheckRepository = healthCheckRepository; - this.objectMapper = new ObjectMapper(); } public Readiness getReadiness() throws DbNotConnectedException { @@ -41,23 +35,33 @@ public Readiness getReadiness() throws DbNotConnectedException { } } - public Cve getCve(String cveId) throws NotFoundException, CantParseJSONException { - var cveEntity = cveRepository.findById(cveId).orElseThrow(NotFoundException::new); + // Not the most elegant solution. This might be replaced by a VIEW in the database, + // or some other feature in spring data jpa? + private SourcePackageCve parseDbResponse(String input) { + var parts = input.split(","); + var packageName = parts[0]; + var cveId = parts[1]; + var cvePublishedDate = parts[2]; + return new SourcePackageCve(cveId, cvePublishedDate, packageName); + } - return cveEntityDataToDomainEntity(cveEntity); + public CveEntity getCve(String cveId) throws NotFoundException { + return cveRepository.findById(cveId).orElseThrow(NotFoundException::new); } - public List getCveForDistribution(String vendor, String product, String codename) throws CantParseJSONException { - var entities = cveRepository.cvesForDistribution(vendor, product, codename); + public List getCveForDistribution(String product, String codename) { + return cveRepository.cvesForDistribution(product, codename).stream().map(this::parseDbResponse).toList(); + } - return entities.stream().map(this::cveEntityDataToDomainEntity).toList(); + public List getCveForDistributionVersion(String product, String version) { + return cveRepository.cvesForDistributionVersion(product, version).stream().map(this::parseDbResponse).toList(); } - private Cve cveEntityDataToDomainEntity(CveEntity cveEntity) throws CantParseJSONException { - try { - return objectMapper.readValue(cveEntity.getData(), Cve.class); - } catch (JsonProcessingException e) { - throw new CantParseJSONException("Failed to parse JSON object into domain classes:\n====\n" + cveEntity.getData() + "\n===="); - } + public List getCveForPackages(String product, String codename, String packages) { + return cveRepository.cvesForPackageList(product, codename,"{"+packages+"}").stream().map(this::parseDbResponse).toList(); + } + + public List getCveForPackagesVersion(String product, String version, String packages) { + return cveRepository.cvesForPackageListVersion(product, version,"{"+packages+"}").stream().map(this::parseDbResponse).toList(); } } diff --git a/src/main/java/io/gardenlinux/glvd/db/CveEntity.java b/src/main/java/io/gardenlinux/glvd/db/CveEntity.java index df1f17f..4ad50e8 100644 --- a/src/main/java/io/gardenlinux/glvd/db/CveEntity.java +++ b/src/main/java/io/gardenlinux/glvd/db/CveEntity.java @@ -65,4 +65,4 @@ public int hashCode() { return result; } -} +} \ No newline at end of file diff --git a/src/main/java/io/gardenlinux/glvd/db/CveRepository.java b/src/main/java/io/gardenlinux/glvd/db/CveRepository.java index dc6e7f1..9106622 100644 --- a/src/main/java/io/gardenlinux/glvd/db/CveRepository.java +++ b/src/main/java/io/gardenlinux/glvd/db/CveRepository.java @@ -1,8 +1,8 @@ package io.gardenlinux.glvd.db; -import io.gardenlinux.glvd.dto.Cve; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import java.util.List; @@ -10,18 +10,76 @@ public interface CveRepository extends JpaRepository { @Query(value = """ SELECT - all_cve.* - 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 + deb_cve.deb_source AS source_package, + all_cve.cve_id AS cve_id, + all_cve."data" ->> 'published' AS cve_published_date + 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_product = :product AND + dist_cpe.deb_codename = :codename AND + deb_cve.debsec_vulnerable = TRUE + ORDER BY + all_cve.cve_id + """, nativeQuery = true) + List cvesForDistribution(@Param("product") String product, @Param("codename") String codename); + + @Query(value = """ + SELECT + deb_cve.deb_source AS source_package, + all_cve.cve_id AS cve_id, + all_cve."data" ->> 'published' AS cve_published_date + 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_product = :product AND + dist_cpe.cpe_version = :version AND + deb_cve.debsec_vulnerable = TRUE + ORDER BY all_cve.cve_id """, nativeQuery = true) - List cvesForDistribution(String vendor, String product, String codename); + List cvesForDistributionVersion(@Param("product") String product, @Param("version") String version); + + @Query(value = """ + SELECT + deb_cve.deb_source AS source_package, + all_cve.cve_id AS cve_id, + all_cve."data" ->> 'published' AS cve_published_date + 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_product = :product AND + dist_cpe.deb_codename = :codename AND + deb_cve.deb_source = ANY(:packages ::TEXT[]) AND + deb_cve.debsec_vulnerable = TRUE + ORDER BY + all_cve.cve_id + """, nativeQuery = true) + List cvesForPackageList(@Param("product") String product, @Param("codename") String codename, @Param("packages") String packages); + + @Query(value = """ + SELECT + deb_cve.deb_source AS source_package, + all_cve.cve_id AS cve_id, + all_cve."data" ->> 'published' AS cve_published_date + 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_product = :product AND + dist_cpe.cpe_version = :version AND + deb_cve.deb_source = ANY(:packages ::TEXT[]) AND + deb_cve.debsec_vulnerable = TRUE + ORDER BY + all_cve.cve_id + """, nativeQuery = true) + List cvesForPackageListVersion(@Param("product") String product, @Param("version") String version, @Param("packages") String packages); } diff --git a/src/main/java/io/gardenlinux/glvd/db/SourcePackageCve.java b/src/main/java/io/gardenlinux/glvd/db/SourcePackageCve.java new file mode 100644 index 0000000..1c234ee --- /dev/null +++ b/src/main/java/io/gardenlinux/glvd/db/SourcePackageCve.java @@ -0,0 +1,64 @@ +package io.gardenlinux.glvd.db; + +import jakarta.annotation.Nonnull; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; + +import java.util.Objects; + +@Entity +public class SourcePackageCve { + + @Id + @Column(name = "cve_id", nullable = false) + private String id; + + @Column(name = "cve_published_date", nullable = false) + @Nonnull + private String cvePublishedDate; + + @Column(name = "source_package", nullable = false) + @Nonnull + private String sourcePackage; + + public SourcePackageCve() { + } + + public SourcePackageCve(String id, @Nonnull String cvePublishedDate, @Nonnull String sourcePackage) { + this.id = id; + this.cvePublishedDate = cvePublishedDate; + this.sourcePackage = sourcePackage; + } + + public String getId() { + return id; + } + + @Nonnull + public String getCvePublishedDate() { + return cvePublishedDate; + } + + @Nonnull + public String getSourcePackage() { + return sourcePackage; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + SourcePackageCve that = (SourcePackageCve) o; + return Objects.equals(id, that.id) && cvePublishedDate.equals(that.cvePublishedDate) && sourcePackage.equals(that.sourcePackage); + } + + @Override + public int hashCode() { + int result = Objects.hashCode(id); + result = 31 * result + cvePublishedDate.hashCode(); + result = 31 * result + sourcePackage.hashCode(); + return result; + } +} diff --git a/src/main/java/io/gardenlinux/glvd/dto/Configuration.java b/src/main/java/io/gardenlinux/glvd/dto/Configuration.java deleted file mode 100644 index b90df38..0000000 --- a/src/main/java/io/gardenlinux/glvd/dto/Configuration.java +++ /dev/null @@ -1,7 +0,0 @@ -package io.gardenlinux.glvd.dto; - -import java.util.List; - -public record Configuration(List nodes) { - -} diff --git a/src/main/java/io/gardenlinux/glvd/dto/CpeMatch.java b/src/main/java/io/gardenlinux/glvd/dto/CpeMatch.java deleted file mode 100644 index 7d6ea3a..0000000 --- a/src/main/java/io/gardenlinux/glvd/dto/CpeMatch.java +++ /dev/null @@ -1,6 +0,0 @@ -package io.gardenlinux.glvd.dto; - -public record CpeMatch(String criteria, Deb deb, boolean vulnerable, String versionStartIncluding, - String versionEndExcluding, String matchCriteriaId) { - -} diff --git a/src/main/java/io/gardenlinux/glvd/dto/Cve.java b/src/main/java/io/gardenlinux/glvd/dto/Cve.java deleted file mode 100644 index b754c23..0000000 --- a/src/main/java/io/gardenlinux/glvd/dto/Cve.java +++ /dev/null @@ -1,10 +0,0 @@ -package io.gardenlinux.glvd.dto; - -import java.util.List; - -public record Cve(String id, String lastModified, String sourceIdentifier, String published, String vulnStatus, - List descriptions, Object metrics, List references, List weaknesses, - List configurations) { - -} - diff --git a/src/main/java/io/gardenlinux/glvd/dto/Deb.java b/src/main/java/io/gardenlinux/glvd/dto/Deb.java deleted file mode 100644 index 5267e32..0000000 --- a/src/main/java/io/gardenlinux/glvd/dto/Deb.java +++ /dev/null @@ -1,5 +0,0 @@ -package io.gardenlinux.glvd.dto; - -public record Deb(String versionLatest, String versionEndExcluding, String cvssSeverity) { - -} diff --git a/src/main/java/io/gardenlinux/glvd/dto/Description.java b/src/main/java/io/gardenlinux/glvd/dto/Description.java deleted file mode 100644 index 48727ee..0000000 --- a/src/main/java/io/gardenlinux/glvd/dto/Description.java +++ /dev/null @@ -1,4 +0,0 @@ -package io.gardenlinux.glvd.dto; - -public record Description(String lang, String value) { -} diff --git a/src/main/java/io/gardenlinux/glvd/dto/Node.java b/src/main/java/io/gardenlinux/glvd/dto/Node.java deleted file mode 100644 index 91c6651..0000000 --- a/src/main/java/io/gardenlinux/glvd/dto/Node.java +++ /dev/null @@ -1,7 +0,0 @@ -package io.gardenlinux.glvd.dto; - -import java.util.List; - -public record Node(List cpeMatch, boolean negate, String operator) { - -} diff --git a/src/main/java/io/gardenlinux/glvd/dto/Reference.java b/src/main/java/io/gardenlinux/glvd/dto/Reference.java deleted file mode 100644 index 75d10bb..0000000 --- a/src/main/java/io/gardenlinux/glvd/dto/Reference.java +++ /dev/null @@ -1,7 +0,0 @@ -package io.gardenlinux.glvd.dto; - -import java.util.List; - -public record Reference(String url, String source, List tags) { - -} diff --git a/src/main/java/io/gardenlinux/glvd/dto/Weakness.java b/src/main/java/io/gardenlinux/glvd/dto/Weakness.java deleted file mode 100644 index 6f62882..0000000 --- a/src/main/java/io/gardenlinux/glvd/dto/Weakness.java +++ /dev/null @@ -1,7 +0,0 @@ -package io.gardenlinux.glvd.dto; - -import java.util.List; - -public record Weakness(String source, String type, List description) { - -} diff --git a/src/main/java/io/gardenlinux/glvd/exceptions/CantParseJSONException.java b/src/main/java/io/gardenlinux/glvd/exceptions/CantParseJSONException.java deleted file mode 100644 index 44fd9c8..0000000 --- a/src/main/java/io/gardenlinux/glvd/exceptions/CantParseJSONException.java +++ /dev/null @@ -1,8 +0,0 @@ -package io.gardenlinux.glvd.exceptions; - -public class CantParseJSONException extends RuntimeException{ - - public CantParseJSONException(String message) { - super(message); - } -} diff --git a/src/test/java/io/gardenlinux/glvd/GlvdControllerTest.java b/src/test/java/io/gardenlinux/glvd/GlvdControllerTest.java index 44ab848..34d8eb6 100644 --- a/src/test/java/io/gardenlinux/glvd/GlvdControllerTest.java +++ b/src/test/java/io/gardenlinux/glvd/GlvdControllerTest.java @@ -99,10 +99,40 @@ public void shouldReturnCvesForBookworm() { .filter(document("getCveForDistro", preprocessRequest(modifyUris().scheme("https").host("glvd.gardenlinux.io").removePort()), preprocessResponse(prettyPrint()))) - .when().port(this.port).get("/v1/cves/debian/debian_linux/bookworm") + .when().port(this.port).get("/v1/cves/debian_linux/bookworm") .then().statusCode(HttpStatus.SC_OK); } + @Test + public void shouldReturnCvesForBookwormByVersion() { + given(this.spec).accept("application/json") + .filter(document("getCveForDistroByVersion", + preprocessRequest(modifyUris().scheme("https").host("glvd.gardenlinux.io").removePort()), + preprocessResponse(prettyPrint()))) + .when().port(this.port).get("/v1/cves/debian_linux/version/12") + .then().statusCode(HttpStatus.SC_OK); + } + + @Test + public void shouldReturnCvesForListOfPackages() { + given(this.spec).accept("application/json") + .filter(document("getCveForPackages", + preprocessRequest(modifyUris().scheme("https").host("glvd.gardenlinux.io").removePort()), + preprocessResponse(prettyPrint()))) + .when().port(this.port).get("/v1/cves/debian_linux/bookworm/packages/dav1d,firefox-esr") + .then().statusCode(HttpStatus.SC_OK); + } + + @Test + public void shouldReturnCvesForListOfPackagesByDistroVersion() { + given(this.spec).accept("application/json") + .filter(document("getCveForPackagesByDistroVersion", + preprocessRequest(modifyUris().scheme("https").host("glvd.gardenlinux.io").removePort()), + preprocessResponse(prettyPrint()))) + .when().port(this.port).get("/v1/cves/debian_linux/version/12/packages/dav1d,firefox-esr") + .then().statusCode(HttpStatus.SC_OK); + } + @Test public void shouldBeReady() { given(this.spec)