// SPDX-FileCopyrightText: the secureCodeBox authors // // SPDX-License-Identifier: Apache-2.0 package io.securecodebox.persistence.defectdojo.service; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.cfg.CoercionAction; import com.fasterxml.jackson.databind.cfg.CoercionInputShape; import io.securecodebox.persistence.defectdojo.config.Config; import io.securecodebox.persistence.defectdojo.exception.LoopException; import io.securecodebox.persistence.defectdojo.http.Foo; import io.securecodebox.persistence.defectdojo.http.ProxyConfigFactory; import io.securecodebox.persistence.defectdojo.model.Engagement; import io.securecodebox.persistence.defectdojo.model.Model; import io.securecodebox.persistence.defectdojo.model.Response; import lombok.Getter; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; import org.springframework.http.converter.FormHttpMessageConverter; import org.springframework.http.converter.ResourceHttpMessageConverter; import org.springframework.http.converter.StringHttpMessageConverter; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.util.LinkedMultiValueMap; import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriComponentsBuilder; import java.net.URI; import java.net.URISyntaxException; import java.util.*; // FIXME: Should be package private bc implementation detail. public abstract class GenericDefectDojoService<T extends Model> { private static final String API_PREFIX = "/api/v2/"; private static final long DEFECT_DOJO_OBJET_LIMIT = 100L; protected Config config; protected ObjectMapper objectMapper; protected ObjectMapper searchStringMapper; @Getter protected RestTemplate restTemplate; public GenericDefectDojoService(Config config) { this.config = config; this.objectMapper = new ObjectMapper(); this.objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); this.objectMapper.coercionConfigFor(Engagement.Status.class).setCoercion(CoercionInputShape.EmptyString, CoercionAction.AsNull); this.objectMapper.findAndRegisterModules(); this.searchStringMapper = new ObjectMapper(); this.searchStringMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); this.searchStringMapper.coercionConfigFor(Engagement.Status.class).setCoercion(CoercionInputShape.EmptyString, CoercionAction.AsNull); this.searchStringMapper.setSerializationInclusion(JsonInclude.Include.NON_DEFAULT); this.restTemplate = this.setupRestTemplate(); } /** * @return The DefectDojo Authentication Header */ private HttpHeaders getDefectDojoAuthorizationHeaders() { return new Foo(config, new ProxyConfigFactory().create()).generateAuthorizationHeaders(); } private RestTemplate setupRestTemplate() { RestTemplate restTemplate = new Foo(config, new ProxyConfigFactory().create()).createRestTemplate(); MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(); converter.setObjectMapper(this.objectMapper); restTemplate.setMessageConverters(List.of( new FormHttpMessageConverter(), new ResourceHttpMessageConverter(), new StringHttpMessageConverter(), converter )); return restTemplate; } protected abstract String getUrlPath(); protected abstract Class<T> getModelClass(); protected abstract Response<T> deserializeList(String response) throws JsonProcessingException; public T get(long id) { var restTemplate = this.getRestTemplate(); HttpEntity<String> payload = new HttpEntity<>(getDefectDojoAuthorizationHeaders()); ResponseEntity<T> response = restTemplate.exchange( this.config.getUrl() + API_PREFIX + this.getUrlPath() + "/" + id, HttpMethod.GET, payload, getModelClass() ); return response.getBody(); } protected Response<T> internalSearch(Map<String, Object> queryParams, long limit, long offset) throws JsonProcessingException, URISyntaxException { var restTemplate = this.getRestTemplate(); HttpEntity<String> payload = new HttpEntity<>(getDefectDojoAuthorizationHeaders()); var mutableQueryParams = new HashMap<String, Object>(queryParams); mutableQueryParams.put("limit", String.valueOf(limit)); mutableQueryParams.put("offset", String.valueOf(offset)); var multiValueMap = new LinkedMultiValueMap<String, String>(); for (var entry : mutableQueryParams.entrySet()) { multiValueMap.set(entry.getKey(), String.valueOf(entry.getValue())); } var url = new URI(this.config.getUrl() + API_PREFIX + this.getUrlPath() + "/"); var uriBuilder = UriComponentsBuilder.fromUri(url).queryParams(multiValueMap); ResponseEntity<String> responseString = restTemplate.exchange( uriBuilder.build(mutableQueryParams), HttpMethod.GET, payload, String.class ); return deserializeList(responseString.getBody()); } public List<T> search(Map<String, Object> queryParams) throws URISyntaxException, JsonProcessingException { List<T> objects = new LinkedList<>(); boolean hasNext = false; long page = 0; do { var response = internalSearch(queryParams, DEFECT_DOJO_OBJET_LIMIT, DEFECT_DOJO_OBJET_LIMIT * page++); objects.addAll(response.getResults()); hasNext = response.getNext() != null; if (page > this.config.getMaxPageCountForGets()) { throw new LoopException("Found too many response object. Quitting after " + (page - 1) + " paginated API pages of " + DEFECT_DOJO_OBJET_LIMIT + " each."); } } while (hasNext); return objects; } public List<T> search() throws URISyntaxException, JsonProcessingException { return search(new LinkedHashMap<>()); } @SuppressWarnings("unchecked") public Optional<T> searchUnique(T searchObject) throws URISyntaxException, JsonProcessingException { Map<String, Object> queryParams = searchStringMapper.convertValue(searchObject, Map.class); var objects = search(queryParams); return objects.stream() .filter(object -> object != null && object.equalsQueryString(queryParams)) .findFirst(); } public Optional<T> searchUnique(Map<String, Object> queryParams) throws URISyntaxException, JsonProcessingException { var objects = search(queryParams); return objects.stream() .filter(object -> object.equalsQueryString(queryParams)) .findFirst(); } public T create(T object) { var restTemplate = this.getRestTemplate(); HttpEntity<T> payload = new HttpEntity<>(object, getDefectDojoAuthorizationHeaders()); ResponseEntity<T> response = restTemplate.exchange(this.config.getUrl() + API_PREFIX + getUrlPath() + "/", HttpMethod.POST, payload, getModelClass()); return response.getBody(); } public void delete(long id) { var restTemplate = this.getRestTemplate(); HttpEntity<String> payload = new HttpEntity<>(getDefectDojoAuthorizationHeaders()); restTemplate.exchange(this.config.getUrl() + API_PREFIX + getUrlPath() + "/" + id + "/", HttpMethod.DELETE, payload, String.class); } public T update(T object, long objectId) { var restTemplate = this.getRestTemplate(); HttpEntity<T> payload = new HttpEntity<>(object, getDefectDojoAuthorizationHeaders()); ResponseEntity<T> response = restTemplate.exchange(this.config.getUrl() + API_PREFIX + getUrlPath() + "/" + objectId + "/", HttpMethod.PUT, payload, getModelClass()); return response.getBody(); } }