diff --git a/Makefile b/Makefile index 37ab8f129b2087e724d404faa13602fb3070853d..153b63f14a31abe46893c9681945685ec6e62ab2 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ setup:; @source .env && rm -f ./config/application/secret.properties && envsubst < config/application/secret.properties.sample > config/application/secret.properties dev-start:; - mvn compile -e quarkus:dev + mvn compile -e quarkus:dev -Dconfig.secret.path=./config/application/secret.properties clean:; mvn clean diff --git a/src/main/java/org/eclipsefoundation/cve/config/CveSourceServiceProvider.java b/src/main/java/org/eclipsefoundation/cve/config/CveSourceServiceProvider.java index 706cbcc3f535f5981f5d56b0b7e2f98b084d8532..8dbcb1b7ce86289805a532f2b7c94ea7f2e2d94c 100644 --- a/src/main/java/org/eclipsefoundation/cve/config/CveSourceServiceProvider.java +++ b/src/main/java/org/eclipsefoundation/cve/config/CveSourceServiceProvider.java @@ -11,11 +11,8 @@ package org.eclipsefoundation.cve.config; import javax.enterprise.context.Dependent; import javax.enterprise.inject.Produces; -import javax.inject.Inject; -import org.eclipse.microprofile.rest.client.inject.RestClient; import org.eclipsefoundation.core.service.CachingService; -import org.eclipsefoundation.cve.api.GithubCveAPI; import org.eclipsefoundation.cve.model.mapper.CveDataMapper; import org.eclipsefoundation.cve.service.CveSourceService; import org.eclipsefoundation.cve.service.GoogleAPIService; @@ -28,8 +25,8 @@ import io.quarkus.arc.DefaultBean; import io.quarkus.arc.properties.IfBuildProperty; /** - * Provider for CVE source service, allows for swapping back to stubbed service for tests/development to avoid having to - * use real data. + * Provider for CVE source service, allows for swapping back to stubbed service + * for tests/development to avoid having to use real data. * * @author Martin Lowe, Zachary Sabourin * @@ -38,14 +35,11 @@ import io.quarkus.arc.properties.IfBuildProperty; public class CveSourceServiceProvider { public static final String PROVIDER_TYPE_PROPERTY_NAME = "eclipse.cve.provider"; - @Inject - @RestClient - GithubCveAPI cveDetailsApi; - @Produces @DefaultBean - public CveSourceService defaultService(GoogleAPIService driveApi, CachingService cache, CveDataMapper mapper, ObjectMapper objectMapper) { - return new DefaultCveSourceService(driveApi, cveDetailsApi, cache, mapper, objectMapper); + public CveSourceService defaultService(GoogleAPIService driveApi, CachingService cache, CveDataMapper mapper, + ObjectMapper objectMapper) { + return new DefaultCveSourceService(driveApi, cache, mapper, objectMapper); } @Produces diff --git a/src/main/java/org/eclipsefoundation/cve/service/CveSourceService.java b/src/main/java/org/eclipsefoundation/cve/service/CveSourceService.java index c16b227e03b5054ba0c33ff63c7bf8610dcfe0dd..45ac4825166bd6bad23e16cc2bdb23b87bad408e 100644 --- a/src/main/java/org/eclipsefoundation/cve/service/CveSourceService.java +++ b/src/main/java/org/eclipsefoundation/cve/service/CveSourceService.java @@ -31,22 +31,25 @@ public interface CveSourceService { public static final Logger LOGGER = LoggerFactory.getLogger(CveSourceService.class); /** - * Gets all CVE data objects, including ones that are not considered completed/public knowledge. + * Gets all CVE data objects, including ones that are not considered + * completed/public knowledge. * * @return all known CVE data objects. */ List getAllCves(); /** - * Checks the cache for a CirclCve with a matching id. Hits the Circl API if no CVE found in cache. + * Checks the cache for a CirclCve with a matching id. Hits the Circl API if no + * CVE found in cache. * * @param id the id to match against CVEs. * @return the Circl CVE data or empty optional if it can't be found. */ - Optional getCveDetails(String id); + CveProjectData getCveDetails(String id); /** - * Retrieves a list of all public CVEs. Any non-public CVEs will be filtered out for security. + * Retrieves a list of all public CVEs. Any non-public CVEs will be filtered out + * for security. * * @return all public CVE entries. */ @@ -57,7 +60,8 @@ public interface CveSourceService { /** * Gets list of CVEs for given project name using case-insensitive matching. * - * @param projectName name of the project to use as CVE filter (in form of 'technology.dash', as an example) + * @param projectName name of the project to use as CVE filter (in form of + * 'technology.dash', as an example) * @param includeInternal whether non-completed CVEs should be included * @return list of CVE entries that match the given filters. */ @@ -69,7 +73,7 @@ public interface CveSourceService { } /** - * Retreives a CVE with matching id. Appends additional Circl CVE summary and score. + * Retreives a CVE with matching id. Appends additional CVE summary and score. * * @param id the id to match against CVEs. * @return CveData object or null. @@ -79,8 +83,8 @@ public interface CveSourceService { } /** - * Does basic shared processing of CVEs for other methods. Returns in process stream for further processing or - * collection. + * Does basic shared processing of CVEs for other methods. Returns in process + * stream for further processing or collection. * * @param includeInternal whether to include non-completed/internal CVE entries * @return stream of CVEs filtered on the status. @@ -90,27 +94,27 @@ public interface CveSourceService { } /** - * Checks the cache for a CirclCve with a matching id. Hits the Circl API if no CVE found in cache. + * Checks the cache for a GH Cve with a matching id. Hits the GH API if no + * CVE found in cache. * * @param id the id to match against CVEs. * @return CirclCveData object or null. */ default CveData augmentCveData(CveData orig) { try { - Optional cveDetails = getCveDetails(orig.getId()); - if (cveDetails.isPresent()) { + CveProjectData cveDetails = getCveDetails(orig.getId()); + if (cveDetails != null) { return CveData .copy(orig) .setSummary(cveDetails - .get() .getDescription() .getDescriptionData() .stream() - .filter(dd -> dd.getLang().equalsIgnoreCase("en")) + .filter(dd -> dd.getLang().equalsIgnoreCase("eng")) .findFirst() .orElse(LocalizedValue.builder().setLang("en").setValue("").build()) .getValue()) - .setCvss(cveDetails.get().getImpact().isPresent() ? cveDetails.get().getImpact().get().getCvss().getBaseScore() + .setCvss(cveDetails.getImpact().isPresent() ? cveDetails.getImpact().get().getCvss().getBaseScore() : null) .build(); } diff --git a/src/main/java/org/eclipsefoundation/cve/service/impl/DefaultCveSourceService.java b/src/main/java/org/eclipsefoundation/cve/service/impl/DefaultCveSourceService.java index 263b3c1497e0b098fea066675d7ffe3d277541ec..26831516fdb898ec542ccfc2b9132dd812c11746 100644 --- a/src/main/java/org/eclipsefoundation/cve/service/impl/DefaultCveSourceService.java +++ b/src/main/java/org/eclipsefoundation/cve/service/impl/DefaultCveSourceService.java @@ -12,17 +12,23 @@ package org.eclipsefoundation.cve.service.impl; +import java.io.IOException; import java.io.InputStream; +import java.time.Duration; import java.util.Collections; import java.util.List; -import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.zip.GZIPInputStream; +import javax.annotation.PostConstruct; +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; import javax.ws.rs.core.Response; +import org.eclipse.microprofile.context.ManagedExecutor; +import org.eclipse.microprofile.rest.client.inject.RestClient; import org.eclipsefoundation.core.service.CachingService; import org.eclipsefoundation.cve.api.GithubCveAPI; import org.eclipsefoundation.cve.model.CveData; @@ -33,6 +39,10 @@ import org.eclipsefoundation.cve.service.GoogleAPIService; import org.jboss.resteasy.specimpl.MultivaluedMapImpl; import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.benmanes.caffeine.cache.AsyncLoadingCache; +import com.github.benmanes.caffeine.cache.Caffeine; + +import io.quarkus.runtime.Startup; /** * Default implementation of the CVE Service @@ -40,47 +50,75 @@ import com.fasterxml.jackson.databind.ObjectMapper; * @author Martin Lowe * */ +@Startup +@ApplicationScoped public class DefaultCveSourceService implements CveSourceService { public static final Pattern CVE_ID_PARTS = Pattern.compile("^CVE-(\\d{4})-(\\d+?)\\d{3}$"); private GoogleAPIService api; - private GithubCveAPI cveDetailsApi; - private CachingService cache; + private CachingService cacheService; private CveDataMapper mapper; private ObjectMapper om; + @Inject + @RestClient + GithubCveAPI cveDetailsApi; + + @Inject + ManagedExecutor executor; + + private AsyncLoadingCache loadingCache; + /** - * Constructor that loads in all of the external services required to retrieve CVE data. + * Constructor that loads in all of the external services required to retrieve + * CVE data. */ - public DefaultCveSourceService(GoogleAPIService api, GithubCveAPI cveDetailsApi, CachingService cache, CveDataMapper mapper, - ObjectMapper om) { + public DefaultCveSourceService(GoogleAPIService api, CachingService cache, CveDataMapper mapper, ObjectMapper om) { this.api = api; this.mapper = mapper; - this.cveDetailsApi = cveDetailsApi; - this.cache = cache; + this.cacheService = cache; this.om = om; } + @PostConstruct + public void init() { + this.loadingCache = Caffeine.newBuilder() + .maximumSize(50) + .expireAfterWrite(Duration.ofMinutes(60)) + .refreshAfterWrite(Duration.ofMinutes(60)) + .buildAsync(this::loadData); + } + @Override public List getAllCves() { - return cache + return cacheService .get("all", new MultivaluedMapImpl<>(), CveData.class, () -> api.getCveSource().stream().map(mapper::toJsonModel).collect(Collectors.toList())) .orElse(Collections.emptyList()); } @Override - public Optional getCveDetails(String id) { - Matcher m = CVE_ID_PARTS.matcher(id); - if (!m.matches()) { - LOGGER.warn("CVE passed with '{}' could not be parsed as a valid CVE ID, returning empty data", id); - return Optional.empty(); + public CveProjectData getCveDetails(String id) { + try { + return getLoadedData(id); + } catch (Exception e) { + throw new RuntimeException(String.format("Could not fetch CVE %s from GitHub", id), e); } - // get the cached data - return cache.get(id, new MultivaluedMapImpl<>(), CveProjectData.class, () -> { - Response r = cveDetailsApi.getCveDetails(m.group(1), m.group(2), id); - Object responseBody = r.getEntity(); - return om.readerFor(CveProjectData.class).readValue(new GZIPInputStream((InputStream) responseBody)); - }); + } + + private CveProjectData getLoadedData(String key) throws Exception { + return loadingCache.get(key).get(); + } + + private CveProjectData loadData(String key) throws IOException { + Matcher matcher = CVE_ID_PARTS.matcher(key); + if (!matcher.matches()) { + LOGGER.warn("CVE passed with '{}' could not be parsed as a valid CVE ID, returning empty data", key); + return null; + } + + Response r = cveDetailsApi.getCveDetails(matcher.group(1), matcher.group(2), key); + Object responseBody = r.getEntity(); + return om.readerFor(CveProjectData.class).readValue(new GZIPInputStream((InputStream) responseBody)); } }