Skip to content

Commit f84887a

Browse files
authored
[backend] feat(stix): artifact management (#3511)
1 parent 3713b93 commit f84887a

40 files changed

Lines changed: 1509 additions & 285 deletions
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package io.openaev.migration;
2+
3+
import java.sql.Statement;
4+
import org.flywaydb.core.api.migration.BaseJavaMigration;
5+
import org.flywaydb.core.api.migration.Context;
6+
import org.springframework.stereotype.Component;
7+
8+
@Component
9+
public class V4_75__Add_artifacts_column_to_security_coverage extends BaseJavaMigration {
10+
@Override
11+
public void migrate(Context context) throws Exception {
12+
try (Statement stmt = context.getConnection().createStatement()) {
13+
stmt.execute(
14+
"ALTER TABLE security_coverages ADD COLUMN IF NOT EXISTS security_coverage_artifacts_refs JSONB;");
15+
16+
stmt.execute(
17+
"""
18+
UPDATE security_coverages
19+
SET security_coverage_vulnerabilities_refs = (
20+
CASE
21+
WHEN security_coverage_vulnerabilities_refs IS NULL\s
22+
OR jsonb_array_length(security_coverage_vulnerabilities_refs) = 0\s
23+
THEN security_coverage_vulnerabilities_refs
24+
25+
ELSE (
26+
SELECT jsonb_agg(
27+
jsonb_set(
28+
elem - 'external_ref',
29+
'{external_refs}',
30+
jsonb_build_array(elem->>'external_ref')
31+
)
32+
)
33+
FROM jsonb_array_elements(security_coverage_vulnerabilities_refs) AS elem
34+
)
35+
END
36+
);
37+
""");
38+
39+
stmt.execute(
40+
"""
41+
UPDATE security_coverages
42+
SET security_coverage_attack_pattern_refs = (
43+
CASE
44+
WHEN security_coverage_attack_pattern_refs IS NULL\s
45+
OR jsonb_array_length(security_coverage_attack_pattern_refs) = 0\s
46+
THEN security_coverage_attack_pattern_refs
47+
48+
ELSE (
49+
SELECT jsonb_agg(
50+
jsonb_set(
51+
elem - 'external_ref',
52+
'{external_refs}',
53+
jsonb_build_array(elem->>'external_ref')
54+
)
55+
)
56+
FROM jsonb_array_elements(security_coverage_attack_pattern_refs) AS elem
57+
)
58+
END
59+
);
60+
""");
61+
62+
stmt.execute(
63+
"""
64+
UPDATE security_coverages
65+
SET security_coverage_indicators_refs = (
66+
CASE
67+
WHEN security_coverage_indicators_refs IS NULL\s
68+
OR jsonb_array_length(security_coverage_indicators_refs) = 0\s
69+
THEN security_coverage_indicators_refs
70+
71+
ELSE (
72+
SELECT jsonb_agg(
73+
jsonb_set(
74+
elem - 'external_ref',
75+
'{external_refs}',
76+
jsonb_build_array(elem->>'external_ref')
77+
)
78+
)
79+
FROM jsonb_array_elements(security_coverage_indicators_refs) AS elem
80+
)
81+
END
82+
);
83+
""");
84+
}
85+
}
86+
}

openaev-api/src/main/java/io/openaev/opencti/client/OpenCTIClient.java

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,27 @@
88
import io.openaev.authorisation.HttpClientFactory;
99
import io.openaev.opencti.client.mutations.Mutation;
1010
import io.openaev.opencti.client.response.Response;
11+
import io.openaev.opencti.client.response.ResponseFile;
1112
import io.openaev.opencti.client.response.fields.Error;
13+
import java.io.ByteArrayInputStream;
1214
import java.io.IOException;
13-
import java.util.HashMap;
14-
import java.util.List;
15-
import java.util.Map;
15+
import java.util.*;
1616
import lombok.RequiredArgsConstructor;
17+
import lombok.extern.slf4j.Slf4j;
1718
import org.apache.hc.client5.http.ClientProtocolException;
19+
import org.apache.hc.client5.http.classic.methods.HttpGet;
1820
import org.apache.hc.client5.http.classic.methods.HttpPost;
1921
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
22+
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
2023
import org.apache.hc.core5.http.ClassicHttpRequest;
24+
import org.apache.hc.core5.http.HttpEntity;
2125
import org.apache.hc.core5.http.io.entity.EntityUtils;
2226
import org.apache.hc.core5.http.io.entity.StringEntity;
2327
import org.apache.http.HttpHeaders;
2428
import org.springframework.stereotype.Component;
2529

2630
@Component
31+
@Slf4j
2732
@RequiredArgsConstructor
2833
public class OpenCTIClient {
2934
private final HttpClientFactory httpClientFactory;
@@ -49,6 +54,30 @@ public Response execute(String url, String authToken, String mutationBody, JsonN
4954
return execute(req);
5055
}
5156

57+
public ResponseFile download(String url, String authToken) throws IOException {
58+
try (CloseableHttpClient client = httpClientFactory.httpClientCustom()) {
59+
HttpGet req = new HttpGet(url);
60+
req.addHeader(HttpHeaders.AUTHORIZATION, "Bearer %s".formatted(authToken));
61+
62+
try (CloseableHttpResponse res = client.execute(req)) {
63+
int statusCode = res.getCode();
64+
if (statusCode != 200) {
65+
log.warn(
66+
String.format("Error downloading file from %s with status code %s", url, statusCode));
67+
return null;
68+
}
69+
70+
HttpEntity entity = res.getEntity();
71+
byte[] content = entity.getContent().readAllBytes();
72+
73+
ResponseFile responseFile = new ResponseFile();
74+
responseFile.setInputStream(new ByteArrayInputStream(content));
75+
responseFile.setSize(content.length);
76+
return responseFile;
77+
}
78+
}
79+
}
80+
5281
public record ExtractedData(int status, String body) {}
5382

5483
private Response execute(ClassicHttpRequest request) throws IOException {
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package io.openaev.opencti.client.response;
2+
3+
import java.io.InputStream;
4+
import lombok.Data;
5+
import lombok.NoArgsConstructor;
6+
7+
@Data
8+
@NoArgsConstructor
9+
public class ResponseFile {
10+
11+
private long size;
12+
13+
private InputStream inputStream;
14+
}

openaev-api/src/main/java/io/openaev/opencti/config/OpenCTIConfig.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,8 @@ public String getApiUrl() {
4343

4444
return String.join("/", urlStripped, GRAPHQL_ENDPOINT_URI);
4545
}
46+
47+
public String getFormattedUrl() {
48+
return url.endsWith("/") ? url : url + "/";
49+
}
4650
}

openaev-api/src/main/java/io/openaev/opencti/service/OpenCTIService.java

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,25 @@
88
import io.openaev.opencti.client.OpenCTIClient;
99
import io.openaev.opencti.client.mutations.*;
1010
import io.openaev.opencti.client.response.Response;
11+
import io.openaev.opencti.client.response.ResponseFile;
1112
import io.openaev.opencti.client.response.fields.Error;
1213
import io.openaev.opencti.config.OpenCTIConfig;
1314
import io.openaev.opencti.connectors.ConnectorBase;
1415
import io.openaev.opencti.connectors.impl.SecurityCoverageConnector;
1516
import io.openaev.opencti.connectors.service.PrivilegeService;
1617
import io.openaev.opencti.errors.ConnectorError;
18+
import io.openaev.rest.document.DocumentService;
19+
import io.openaev.rest.document.form.DocumentCreateInput;
20+
import io.openaev.rest.tag.TagService;
21+
import io.openaev.rest.tag.form.TagCreateInput;
1722
import io.openaev.stix.objects.Bundle;
1823
import java.io.IOException;
24+
import java.net.URLEncoder;
25+
import java.nio.charset.StandardCharsets;
1926
import java.time.Instant;
27+
import java.util.ArrayList;
2028
import java.util.List;
29+
import java.util.Set;
2130
import java.util.stream.Collectors;
2231
import lombok.RequiredArgsConstructor;
2332
import lombok.extern.slf4j.Slf4j;
@@ -32,6 +41,8 @@ public class OpenCTIService {
3241
private final OpenCTIClient openCTIClient;
3342
private final ObjectMapper mapper;
3443
private final PrivilegeService privilegeService;
44+
private final TagService tagService;
45+
private final DocumentService documentService;
3546

3647
private void applyJwksIfApplicable(ConnectorBase connector, String jwks) {
3748
if (connector instanceof SecurityCoverageConnector scc) {
@@ -244,4 +255,53 @@ public void createReport(
244255
execution.addTrace(getNewErrorTrace("Fail to POST", ExecutionTraceAction.COMPLETE));
245256
}
246257
}
258+
259+
/**
260+
* Download and save file from OpenCTI
261+
*
262+
* @param uri of the file
263+
* @param name of the file to download
264+
* @param mimeType of the file to download
265+
* @return the document created from downloaded file
266+
*/
267+
public Document downloadAndSaveFile(String uri, String name, String mimeType) {
268+
try {
269+
ResponseFile octiResponseFile = downloadFile(uri);
270+
271+
if (octiResponseFile != null) {
272+
Tag openCtiTag = getOpenCTITag();
273+
DocumentCreateInput documentCreateInput = new DocumentCreateInput();
274+
documentCreateInput.setDescription(name);
275+
if (openCtiTag != null) {
276+
documentCreateInput.setTagIds(new ArrayList<>(Set.of(openCtiTag.getId())));
277+
}
278+
279+
return documentService.upsert(
280+
name,
281+
octiResponseFile.getInputStream(),
282+
octiResponseFile.getSize(),
283+
mimeType,
284+
documentCreateInput);
285+
}
286+
} catch (Exception e) {
287+
log.error(
288+
String.format(
289+
"Error while upserting document from OpenCTI file (uri=%s, name=%s, mimeType=%s)",
290+
uri, name, mimeType),
291+
e);
292+
}
293+
return null;
294+
}
295+
296+
private ResponseFile downloadFile(String uri) throws IOException {
297+
return openCTIClient.download(
298+
classicOpenCTIConfig.getFormattedUrl() + URLEncoder.encode(uri, StandardCharsets.UTF_8),
299+
classicOpenCTIConfig.getToken());
300+
}
301+
302+
private Tag getOpenCTITag() {
303+
TagCreateInput tagCreateInput = new TagCreateInput();
304+
tagCreateInput.setName(Tag.OPENCTI_TAG_NAME);
305+
return tagService.upsertTag(tagCreateInput);
306+
}
247307
}

openaev-api/src/main/java/io/openaev/rest/attack_pattern/service/AttackPatternService.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package io.openaev.rest.attack_pattern.service;
22

33
import static io.openaev.helper.StreamHelper.fromIterable;
4-
import static io.openaev.utils.SecurityCoverageUtils.getExternalIds;
54

65
import com.fasterxml.jackson.databind.JsonNode;
76
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -12,6 +11,7 @@
1211
import io.openaev.ee.EnterpriseEditionService;
1312
import io.openaev.rest.attack_pattern.form.AnalysisResultFromTTPExtractionAIWebserviceOutput;
1413
import io.openaev.rest.exception.ElementNotFoundException;
14+
import io.openaev.utils.SecurityCoverageUtils;
1515
import jakarta.annotation.Resource;
1616
import java.io.IOException;
1717
import java.nio.charset.StandardCharsets;
@@ -44,6 +44,7 @@ public class AttackPatternService {
4444
private final AttackPatternRepository attackPatternRepository;
4545
private final EnterpriseEditionService enterpriseEditionService;
4646
private final RestTemplate restTemplate;
47+
private final SecurityCoverageUtils securityCoverageUtils;
4748

4849
/**
4950
* Call the TTP Extraction AI Webservice to analyze files and text input.
@@ -245,7 +246,7 @@ public List<String> searchAttackPatternWithTTPAIWebservice(
245246
*/
246247
public Map<String, AttackPattern> fetchInternalAttackPatternIds(
247248
Set<StixRefToExternalRef> stixRefs) {
248-
return getAttackPatternsByExternalIds(getExternalIds(stixRefs)).stream()
249+
return getAttackPatternsByExternalIds(securityCoverageUtils.getExternalIds(stixRefs)).stream()
249250
.collect(Collectors.toMap(attack -> attack.getId(), Function.identity()));
250251
}
251252

0 commit comments

Comments
 (0)