Skip to content

Commit b0e6247

Browse files
committed
#36 Separate API from implementation to decouple API clients
Signed-off-by: Sven Strittmatter <sven.strittmatter@iteratec.com>
1 parent 0c44027 commit b0e6247

File tree

3 files changed

+185
-147
lines changed

3 files changed

+185
-147
lines changed
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
// Copyright 2021 iteratec GmbH
2+
// SPDX-FileCopyrightText: 2023 iteratec GmbH
3+
//
4+
// SPDX-License-Identifier: Apache-2.0
5+
6+
package io.securecodebox.persistence.defectdojo.service;
7+
8+
import io.securecodebox.persistence.defectdojo.ScanType;
9+
import io.securecodebox.persistence.defectdojo.config.DefectDojoConfig;
10+
import io.securecodebox.persistence.defectdojo.exceptions.DefectDojoPersistenceException;
11+
import io.securecodebox.persistence.defectdojo.models.ScanFile;
12+
import lombok.NonNull;
13+
import org.apache.http.HttpHost;
14+
import org.apache.http.auth.AuthScope;
15+
import org.apache.http.auth.UsernamePasswordCredentials;
16+
import org.apache.http.impl.client.BasicCredentialsProvider;
17+
import org.apache.http.impl.client.HttpClientBuilder;
18+
import org.apache.http.impl.client.ProxyAuthenticationStrategy;
19+
import org.springframework.core.io.ByteArrayResource;
20+
import org.springframework.http.HttpEntity;
21+
import org.springframework.http.HttpHeaders;
22+
import org.springframework.http.HttpMethod;
23+
import org.springframework.http.MediaType;
24+
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
25+
import org.springframework.http.converter.FormHttpMessageConverter;
26+
import org.springframework.http.converter.ResourceHttpMessageConverter;
27+
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
28+
import org.springframework.util.LinkedMultiValueMap;
29+
import org.springframework.util.MultiValueMap;
30+
import org.springframework.web.client.HttpClientErrorException;
31+
import org.springframework.web.client.RestTemplate;
32+
33+
import java.nio.charset.StandardCharsets;
34+
import java.util.List;
35+
36+
final class DefaultImportScanService implements ImportScanService {
37+
private final String defectDojoUrl;
38+
private final String defectDojoApiKey;
39+
40+
/**
41+
* Dedicated constructor.
42+
*
43+
* @param config not {@code null}
44+
*/
45+
DefaultImportScanService(final @NonNull DefectDojoConfig config) {
46+
super();
47+
this.defectDojoUrl = config.getUrl();
48+
this.defectDojoApiKey = config.getApiKey();
49+
}
50+
51+
/**
52+
* The DefectDojo Authentication Header
53+
*
54+
* @return never {@code null}
55+
*/
56+
HttpHeaders createDefectDojoAuthorizationHeaders() {
57+
final var authorizationHeader = new HttpHeaders();
58+
authorizationHeader.set(HttpHeaders.AUTHORIZATION, String.format("Token %s", defectDojoApiKey));
59+
return authorizationHeader;
60+
}
61+
62+
private RestTemplate createRestTemplate() {
63+
if (System.getProperty("http.proxyUser") != null && System.getProperty("http.proxyPassword") != null) {
64+
// Configuring Proxy Authentication explicitly as it isn't done by default for spring rest templates :(
65+
final var credentials = new BasicCredentialsProvider();
66+
credentials.setCredentials(
67+
new AuthScope(System.getProperty("http.proxyHost"), Integer.parseInt(System.getProperty("http.proxyPort"))),
68+
new UsernamePasswordCredentials(System.getProperty("http.proxyUser"), System.getProperty("http.proxyPassword"))
69+
);
70+
71+
final var clientBuilder = HttpClientBuilder.create();
72+
73+
clientBuilder.useSystemProperties();
74+
clientBuilder.setProxy(new HttpHost(System.getProperty("http.proxyHost"), Integer.parseInt(System.getProperty("http.proxyPort"))));
75+
clientBuilder.setDefaultCredentialsProvider(credentials);
76+
clientBuilder.setProxyAuthenticationStrategy(new ProxyAuthenticationStrategy());
77+
78+
final var factory = new HttpComponentsClientHttpRequestFactory();
79+
factory.setHttpClient(clientBuilder.build());
80+
return new RestTemplate(factory);
81+
} else {
82+
return new RestTemplate();
83+
}
84+
}
85+
86+
/*
87+
* Before version 1.5.4. testName (in DefectDojo _test_type_) must be defectDojoScanName, afterward, you can have something else.
88+
*/
89+
private ImportScanResponse createFindings(ScanFile scanFile, String endpoint, long lead, String currentDate, ScanType scanType, long testType, MultiValueMap<String, String> options) {
90+
var restTemplate = this.createRestTemplate();
91+
HttpHeaders headers = createDefectDojoAuthorizationHeaders();
92+
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
93+
restTemplate.setMessageConverters(List.of(
94+
new FormHttpMessageConverter(),
95+
new ResourceHttpMessageConverter(),
96+
new MappingJackson2HttpMessageConverter())
97+
);
98+
99+
final var body = new LinkedMultiValueMap<String, Object>();
100+
101+
body.add("lead", Long.toString(lead));
102+
body.add("scan_date", currentDate);
103+
body.add("scan_type", scanType.getTestType());
104+
body.add("close_old_findings", "true");
105+
body.add("skip_duplicates", "false");
106+
body.add("test_type", String.valueOf(testType));
107+
108+
for (final var optionName : options.keySet()) {
109+
body.remove(optionName);
110+
}
111+
112+
// FIXME: Workaround due to type incompatibility of MultiValueMap<String, String> and MultiValueMap<String, Object>.
113+
for (final var option : options.entrySet()) {
114+
body.add(option.getKey(), option.getValue());
115+
}
116+
117+
try {
118+
ByteArrayResource contentsAsResource = new ByteArrayResource(scanFile.getContent().getBytes(StandardCharsets.UTF_8)) {
119+
@Override
120+
public String getFilename() {
121+
return scanFile.getName();
122+
}
123+
};
124+
125+
// FIXME Why do we add the whole byte array resiurce here as object? Is not simply the file name sufficient here? Then we could use <String, String>
126+
body.add("file", contentsAsResource);
127+
128+
// FIXME: We do not define the the type T of the body here!
129+
final var payload = new HttpEntity<>(body, headers);
130+
131+
return restTemplate.exchange(defectDojoUrl + "/api/v2/" + endpoint + "/", HttpMethod.POST, payload, ImportScanResponse.class).getBody();
132+
} catch (HttpClientErrorException e) {
133+
throw new DefectDojoPersistenceException("Failed to attach findings to engagement.");
134+
}
135+
}
136+
137+
@Override
138+
public ImportScanResponse importScan(ScanFile scanFile, long engagementId, long lead, String currentDate, ScanType scanType, long testType) {
139+
final var options = new LinkedMultiValueMap<String, String>();
140+
options.add("engagement", Long.toString(engagementId)); // FIXME Seems to be duplicated bc it is done again in the overloaded method.
141+
142+
return this.importScan(scanFile, engagementId, lead, currentDate, scanType, testType, options);
143+
}
144+
145+
@Override
146+
public ImportScanResponse importScan(ScanFile scanFile, long engagementId, long lead, String currentDate, ScanType scanType, long testType, MultiValueMap<String, String> options) {
147+
options.add("engagement", Long.toString(engagementId));
148+
149+
return this.createFindings(scanFile, "import-scan", lead, currentDate, scanType, testType, options);
150+
}
151+
152+
@Override
153+
public ImportScanResponse reimportScan(ScanFile scanFile, long testId, long lead, String currentDate, ScanType scanType, long testType) {
154+
final var options = new LinkedMultiValueMap<String, String>();
155+
options.add("test", Long.toString(testId)); // FIXME Seems to be duplicated bc it is done again in the overloaded method.
156+
157+
return this.reimportScan(scanFile, testId, lead, currentDate, scanType, testType, options);
158+
}
159+
160+
@Override
161+
public ImportScanResponse reimportScan(ScanFile scanFile, long testId, long lead, String currentDate, ScanType scanType, long testType, MultiValueMap<String, String> options) {
162+
options.add("test", Long.toString(testId));
163+
164+
return this.createFindings(scanFile, "reimport-scan", lead, currentDate, scanType, testType, options);
165+
166+
167+
}
168+
}

src/main/java/io/securecodebox/persistence/defectdojo/service/ImportScanService.java

Lines changed: 13 additions & 141 deletions
Original file line numberDiff line numberDiff line change
@@ -8,163 +8,35 @@
88
import com.fasterxml.jackson.annotation.JsonProperty;
99
import io.securecodebox.persistence.defectdojo.ScanType;
1010
import io.securecodebox.persistence.defectdojo.config.DefectDojoConfig;
11-
import io.securecodebox.persistence.defectdojo.exceptions.DefectDojoPersistenceException;
1211
import io.securecodebox.persistence.defectdojo.models.ScanFile;
1312
import lombok.Data;
1413
import lombok.NonNull;
15-
import org.apache.http.HttpHost;
16-
import org.apache.http.auth.AuthScope;
17-
import org.apache.http.auth.UsernamePasswordCredentials;
18-
import org.apache.http.impl.client.BasicCredentialsProvider;
19-
import org.apache.http.impl.client.HttpClientBuilder;
20-
import org.apache.http.impl.client.ProxyAuthenticationStrategy;
21-
import org.springframework.core.io.ByteArrayResource;
22-
import org.springframework.http.HttpEntity;
23-
import org.springframework.http.HttpHeaders;
24-
import org.springframework.http.HttpMethod;
25-
import org.springframework.http.MediaType;
26-
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
27-
import org.springframework.http.converter.FormHttpMessageConverter;
28-
import org.springframework.http.converter.ResourceHttpMessageConverter;
29-
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
30-
import org.springframework.util.LinkedMultiValueMap;
3114
import org.springframework.util.MultiValueMap;
32-
import org.springframework.web.client.HttpClientErrorException;
33-
import org.springframework.web.client.RestTemplate;
34-
35-
import java.nio.charset.StandardCharsets;
36-
import java.util.List;
37-
38-
final class ImportScanService {
39-
40-
private final String defectDojoUrl;
41-
private final String defectDojoApiKey;
42-
43-
/**
44-
* Dedicated constructor.
45-
*
46-
* @param config not {@code null}
47-
*/
48-
public ImportScanService(final @NonNull DefectDojoConfig config) {
49-
super();
50-
this.defectDojoUrl = config.getUrl();
51-
this.defectDojoApiKey = config.getApiKey();
52-
}
5315

16+
/**
17+
* Service to re/import findings into DefectDojo
18+
*/
19+
public interface ImportScanService {
5420
/**
55-
* The DefectDojo Authentication Header
21+
* Factory method to create new instance of service default implementation
5622
*
23+
* @param config must not be {@code null}
5724
* @return never {@code null}
5825
*/
59-
HttpHeaders createDefectDojoAuthorizationHeaders() {
60-
final var authorizationHeader = new HttpHeaders();
61-
authorizationHeader.set(HttpHeaders.AUTHORIZATION, String.format("Token %s", defectDojoApiKey));
62-
return authorizationHeader;
63-
}
64-
65-
protected RestTemplate createRestTemplate() {
66-
if (System.getProperty("http.proxyUser") != null && System.getProperty("http.proxyPassword") != null) {
67-
// Configuring Proxy Authentication explicitly as it isn't done by default for spring rest templates :(
68-
final var credentials = new BasicCredentialsProvider();
69-
credentials.setCredentials(
70-
new AuthScope(System.getProperty("http.proxyHost"), Integer.parseInt(System.getProperty("http.proxyPort"))),
71-
new UsernamePasswordCredentials(System.getProperty("http.proxyUser"), System.getProperty("http.proxyPassword"))
72-
);
73-
74-
final var clientBuilder = HttpClientBuilder.create();
75-
76-
clientBuilder.useSystemProperties();
77-
clientBuilder.setProxy(new HttpHost(System.getProperty("http.proxyHost"), Integer.parseInt(System.getProperty("http.proxyPort"))));
78-
clientBuilder.setDefaultCredentialsProvider(credentials);
79-
clientBuilder.setProxyAuthenticationStrategy(new ProxyAuthenticationStrategy());
80-
81-
final var factory = new HttpComponentsClientHttpRequestFactory();
82-
factory.setHttpClient(clientBuilder.build());
83-
return new RestTemplate(factory);
84-
} else {
85-
return new RestTemplate();
86-
}
87-
}
88-
89-
/*
90-
* Before version 1.5.4. testName (in DefectDojo _test_type_) must be defectDojoScanName, afterward, you can have something else.
91-
*/
92-
protected ImportScanResponse createFindings(ScanFile scanFile, String endpoint, long lead, String currentDate, ScanType scanType, long testType, MultiValueMap<String, String> options) {
93-
var restTemplate = this.createRestTemplate();
94-
HttpHeaders headers = createDefectDojoAuthorizationHeaders();
95-
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
96-
restTemplate.setMessageConverters(List.of(
97-
new FormHttpMessageConverter(),
98-
new ResourceHttpMessageConverter(),
99-
new MappingJackson2HttpMessageConverter())
100-
);
101-
102-
final var body = new LinkedMultiValueMap<String, Object>();
103-
104-
body.add("lead", Long.toString(lead));
105-
body.add("scan_date", currentDate);
106-
body.add("scan_type", scanType.getTestType());
107-
body.add("close_old_findings", "true");
108-
body.add("skip_duplicates", "false");
109-
body.add("test_type", String.valueOf(testType));
110-
111-
for (final var optionName : options.keySet()) {
112-
body.remove(optionName);
113-
}
114-
115-
// FIXME: Workaround due to type incompatibility of MultiValueMap<String, String> and MultiValueMap<String, Object>.
116-
for (final var option : options.entrySet()) {
117-
body.add(option.getKey(), option.getValue());
118-
}
119-
120-
try {
121-
ByteArrayResource contentsAsResource = new ByteArrayResource(scanFile.getContent().getBytes(StandardCharsets.UTF_8)) {
122-
@Override
123-
public String getFilename() {
124-
return scanFile.getName();
125-
}
126-
};
127-
128-
// FIXME Why do we add the whole byte array resiurce here as object? Is not simply the file name sufficient here? Then we could use <String, String>
129-
body.add("file", contentsAsResource);
130-
131-
// FIXME: We do not define the the type T of the body here!
132-
final var payload = new HttpEntity<>(body, headers);
133-
134-
return restTemplate.exchange(defectDojoUrl + "/api/v2/" + endpoint + "/", HttpMethod.POST, payload, ImportScanResponse.class).getBody();
135-
} catch (HttpClientErrorException e) {
136-
throw new DefectDojoPersistenceException("Failed to attach findings to engagement.");
137-
}
26+
default ImportScanService createDefault(@NonNull DefectDojoConfig config) {
27+
return new DefaultImportScanService(config);
13828
}
13929

140-
public ImportScanResponse importScan(ScanFile scanFile, long engagementId, long lead, String currentDate, ScanType scanType, long testType) {
141-
final var options = new LinkedMultiValueMap<String, String>();
142-
options.add("engagement", Long.toString(engagementId)); // FIXME Seems to be duplicated bc it is done again in the overloaded method.
30+
ImportScanResponse importScan(ScanFile scanFile, long engagementId, long lead, String currentDate, ScanType scanType, long testType);
14331

144-
return this.importScan(scanFile, engagementId, lead, currentDate, scanType, testType, options);
145-
}
146-
147-
public ImportScanResponse importScan(ScanFile scanFile, long engagementId, long lead, String currentDate, ScanType scanType, long testType, MultiValueMap<String, String> options) {
148-
options.add("engagement", Long.toString(engagementId));
149-
150-
return this.createFindings(scanFile, "import-scan", lead, currentDate, scanType, testType, options);
151-
}
32+
ImportScanResponse importScan(ScanFile scanFile, long engagementId, long lead, String currentDate, ScanType scanType, long testType, MultiValueMap<String, String> options);
15233

153-
public ImportScanResponse reimportScan(ScanFile scanFile, long testId, long lead, String currentDate, ScanType scanType, long testType) {
154-
final var options = new LinkedMultiValueMap<String, String>();
155-
options.add("test", Long.toString(testId)); // FIXME Seems to be duplicated bc it is done again in the overloaded method.
34+
ImportScanResponse reimportScan(ScanFile scanFile, long testId, long lead, String currentDate, ScanType scanType, long testType);
15635

157-
return this.reimportScan(scanFile, testId, lead, currentDate, scanType, testType, options);
158-
}
159-
160-
public ImportScanResponse reimportScan(ScanFile scanFile, long testId, long lead, String currentDate, ScanType scanType, long testType, MultiValueMap<String, String> options) {
161-
options.add("test", Long.toString(testId));
162-
163-
return this.createFindings(scanFile, "reimport-scan", lead, currentDate, scanType, testType, options);
164-
}
36+
ImportScanResponse reimportScan(ScanFile scanFile, long testId, long lead, String currentDate, ScanType scanType, long testType, MultiValueMap<String, String> options);
16537

16638
@Data
167-
public static class ImportScanResponse {
39+
class ImportScanResponse {
16840
@JsonProperty
16941
protected Boolean verified;
17042

src/test/java/io/securecodebox/persistence/defectdojo/service/ImportScanServiceTest.java renamed to src/test/java/io/securecodebox/persistence/defectdojo/service/DefaultImportScanServiceTest.java

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,29 @@
11
package io.securecodebox.persistence.defectdojo.service;
22

33
import io.securecodebox.persistence.defectdojo.config.DefectDojoConfig;
4-
import org.junit.jupiter.api.Assertions;
54
import org.junit.jupiter.api.Test;
65
import org.springframework.http.HttpHeaders;
76

87
import static org.junit.jupiter.api.Assertions.*;
98
import static org.hamcrest.MatcherAssert.assertThat;
10-
import static org.hamcrest.Matchers.*;
119

1210
/**
13-
* Tests for {@link ImportScanService}
11+
* Tests for {@link DefaultImportScanService}
1412
*/
15-
class ImportScanServiceTest {
13+
class DefaultImportScanServiceTest {
1614
private final DefectDojoConfig config = new DefectDojoConfig(
1715
"url",
1816
"apiKey",
1917
"username",
2018
23,
2119
42L
2220
);
23-
private final ImportScanService sut = new ImportScanService(config);
21+
private final DefaultImportScanService sut = new DefaultImportScanService(config);
2422

2523
@Test
2624
void constructorShouldThrowExceptionOnNullConfig() {
2725
assertThrows(NullPointerException.class, () -> {
28-
new ImportScanService(null);
26+
new DefaultImportScanService(null);
2927
});
3028
}
3129

0 commit comments

Comments
 (0)