Skip to content

Commit 291e75d

Browse files
authored
[backend] fix(securitycoverage): ensure forcible opencti tag in security coverage scenario (#4395)
Signed-off-by: Antoine MAZEAS <[email protected]>
1 parent d193dcf commit 291e75d

File tree

5 files changed

+316
-15
lines changed

5 files changed

+316
-15
lines changed

openaev-api/src/main/java/io/openaev/service/stix/SecurityCoverageService.java

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -110,12 +110,19 @@ public SecurityCoverage buildSecurityCoverageFromStix(String stixJson)
110110

111111
// Optional fields
112112
stixCoverageObj.setIfPresent(STIX_DESCRIPTION, securityCoverage::setDescription);
113-
stixCoverageObj.setIfSetPresent(
114-
CommonProperties.LABELS.toString(),
115-
labels -> {
116-
labels.add(OPENCTI_TAG_NAME);
117-
securityCoverage.setLabels(labels);
118-
});
113+
114+
// labels
115+
Set<String> labels = new HashSet<>();
116+
if (stixCoverageObj.hasProperty(CommonProperties.LABELS)
117+
&& stixCoverageObj.getProperty(CommonProperties.LABELS).getValue() != null) {
118+
for (StixString stixString :
119+
(List<StixString>) stixCoverageObj.getProperty(CommonProperties.LABELS).getValue()) {
120+
labels.add(stixString.getValue());
121+
}
122+
}
123+
// force opencti
124+
labels.add(OPENCTI_TAG_NAME);
125+
securityCoverage.setLabels(labels);
119126

120127
// Extract Attack Patterns
121128
securityCoverage.setAttackPatternRefs(

openaev-api/src/test/java/io/openaev/api/stix_process/StixApiTest.java

Lines changed: 134 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,12 @@
1818
import com.jayway.jsonpath.JsonPath;
1919
import io.openaev.IntegrationTest;
2020
import io.openaev.database.model.*;
21+
import io.openaev.database.model.Tag;
2122
import io.openaev.database.repository.InjectRepository;
2223
import io.openaev.database.repository.ScenarioRepository;
2324
import io.openaev.database.repository.SecurityCoverageRepository;
25+
import io.openaev.database.repository.TagRepository;
26+
import io.openaev.service.AssetGroupService;
2427
import io.openaev.utils.fixtures.*;
2528
import io.openaev.utils.fixtures.composers.*;
2629
import io.openaev.utils.fixtures.files.AttackPatternFixture;
@@ -56,19 +59,23 @@ class StixApiTest extends IntegrationTest {
5659

5760
@Autowired private ScenarioRepository scenarioRepository;
5861
@Autowired private InjectRepository injectRepository;
62+
@Autowired private TagRepository tagRepository;
5963
@Autowired private SecurityCoverageRepository securityCoverageRepository;
64+
@Autowired private AssetGroupService assetGroupService;
6065

6166
@Autowired private AttackPatternComposer attackPatternComposer;
6267
@Autowired private VulnerabilityComposer vulnerabilityComposer;
6368
@Autowired private TagRuleComposer tagRuleComposer;
6469
@Autowired private AssetGroupComposer assetGroupComposer;
6570
@Autowired private EndpointComposer endpointComposer;
71+
@Autowired private PayloadComposer payloadComposer;
6672
@Autowired private InjectorContractComposer injectorContractComposer;
6773
@Autowired private TagComposer tagComposer;
6874

6975
@Autowired private InjectorFixture injectorFixture;
7076

7177
private String stixSecurityCoverage;
78+
private String stixSecurityCoverageNoLabels;
7279
private String stixSecurityCoverageWithoutTtps;
7380
private String stixSecurityCoverageWithoutVulns;
7481
private String stixSecurityCoverageWithoutObjects;
@@ -79,9 +86,21 @@ class StixApiTest extends IntegrationTest {
7986
@BeforeEach
8087
void setUp() throws Exception {
8188
attackPatternComposer.reset();
89+
vulnerabilityComposer.reset();
90+
tagRuleComposer.reset();
91+
endpointComposer.reset();
92+
assetGroupComposer.reset();
93+
payloadComposer.reset();
94+
injectorContractComposer.reset();
95+
tagComposer.reset();
96+
8297
stixSecurityCoverage =
8398
loadJsonWithStixObjectsAsText("src/test/resources/stix-bundles/security-coverage.json");
8499

100+
stixSecurityCoverageNoLabels =
101+
loadJsonWithStixObjectsAsText(
102+
"src/test/resources/stix-bundles/security-coverage-no-labels.json");
103+
85104
stixSecurityCoverageWithoutTtps =
86105
loadJsonWithStixObjectsAsText(
87106
"src/test/resources/stix-bundles/security-coverage-without-ttps.json");
@@ -98,9 +117,6 @@ void setUp() throws Exception {
98117
loadJsonWithStixObjectsAsText(
99118
"src/test/resources/stix-bundles/security-coverage-only-vulns.json");
100119

101-
attackPatternComposer
102-
.forAttackPattern(AttackPatternFixture.createAttackPatternsWithExternalId(T_1531))
103-
.persist();
104120
attackPatternComposer
105121
.forAttackPattern(AttackPatternFixture.createAttackPatternsWithExternalId(T_1003))
106122
.persist();
@@ -170,16 +186,125 @@ void setUp() throws Exception {
170186
.persist();
171187
}
172188

173-
@AfterEach
174-
void afterEach() {
175-
attackPatternComposer.reset();
176-
vulnerabilityComposer.reset();
177-
}
178-
179189
@Nested
180190
@DisplayName("Import STIX Bundles")
181191
class ImportStixBundles {
182192

193+
@Test
194+
@DisplayName(
195+
"When Security Coverage SDO has no labels property, should force adding opencti tag to scenario")
196+
void whenSecurityCoverageSDOHasNoLabelsProperty_shouldForceAddingOpenctiTagToScenario()
197+
throws Exception {
198+
String response =
199+
mvc.perform(
200+
post(STIX_URI + "/process-bundle")
201+
.contentType(MediaType.APPLICATION_JSON)
202+
.content(stixSecurityCoverageNoLabels))
203+
.andExpect(status().isOk())
204+
.andReturn()
205+
.getResponse()
206+
.getContentAsString();
207+
208+
assertThat(response).isNotBlank();
209+
String scenarioId = JsonPath.read(response, "$.scenarioId");
210+
Scenario createdScenario = scenarioRepository.findById(scenarioId).orElseThrow();
211+
Tag openctiTag = tagRepository.findByName(OPENCTI_TAG_NAME).get();
212+
213+
assertThat(createdScenario.getTags()).contains(openctiTag);
214+
}
215+
216+
@Test
217+
@DisplayName(
218+
"When Security Coverage SDO has labels property but not the opencti value, should force adding opencti tag to scenario")
219+
void
220+
whenSecurityCoverageSDOHasLabelsPropertyButNotTheOpenctiValue_shouldForceAddingOpenctiTagToScenario()
221+
throws Exception {
222+
String bundleWithoutOpenctiLabel = stixSecurityCoverage.replace("opencti", "some-label");
223+
224+
String response =
225+
mvc.perform(
226+
post(STIX_URI + "/process-bundle")
227+
.contentType(MediaType.APPLICATION_JSON)
228+
.content(bundleWithoutOpenctiLabel))
229+
.andExpect(status().isOk())
230+
.andReturn()
231+
.getResponse()
232+
.getContentAsString();
233+
234+
assertThat(response).isNotBlank();
235+
String scenarioId = JsonPath.read(response, "$.scenarioId");
236+
Scenario createdScenario = scenarioRepository.findById(scenarioId).orElseThrow();
237+
Tag openctiTag = tagRepository.findByName(OPENCTI_TAG_NAME).get();
238+
239+
assertThat(createdScenario.getTags()).contains(openctiTag);
240+
}
241+
242+
@Test
243+
@DisplayName("Eligible asset groups are assigned by tag rule")
244+
void eligibleAssetGroupsAreAssignedByTagRule() throws Exception {
245+
String label = "custom-label";
246+
tagRuleComposer
247+
.forTagRule(TagRuleFixture.createDefaultTagRule())
248+
.withTag(tagComposer.forTag(TagFixture.getTagWithText(label)))
249+
.withAssetGroup(
250+
assetGroupComposer
251+
.forAssetGroup(
252+
AssetGroupFixture.createDefaultAssetGroup("%s asset group".formatted(label)))
253+
.withAsset(endpointComposer.forEndpoint(EndpointFixture.createEndpoint())))
254+
.persist();
255+
256+
AttackPatternComposer.Composer attackPatternWrapper =
257+
attackPatternComposer.forAttackPattern(
258+
AttackPatternFixture.createAttackPatternsWithExternalId(T_1531));
259+
injectorContractComposer
260+
.forInjectorContract(
261+
InjectorContractFixture.createInjectorContractWithPlatforms(
262+
List.of(Endpoint.PLATFORM_TYPE.Windows).toArray(Endpoint.PLATFORM_TYPE[]::new)))
263+
.withAttackPattern(attackPatternWrapper)
264+
.withPayload(
265+
payloadComposer
266+
.forPayload(PayloadFixture.createDefaultCommand())
267+
.withAttackPattern(attackPatternWrapper))
268+
.persist();
269+
270+
String bundleWithCustomLabel = stixSecurityCoverage.replace(OPENCTI_TAG_NAME, label);
271+
272+
entityManager.flush();
273+
entityManager.clear();
274+
275+
String response =
276+
mvc.perform(
277+
post(STIX_URI + "/process-bundle")
278+
.contentType(MediaType.APPLICATION_JSON)
279+
.content(bundleWithCustomLabel))
280+
.andExpect(status().isOk())
281+
.andReturn()
282+
.getResponse()
283+
.getContentAsString();
284+
285+
entityManager.flush();
286+
entityManager.clear();
287+
288+
assertThat(response).isNotBlank();
289+
String scenarioId = JsonPath.read(response, "$.scenarioId");
290+
Scenario createdScenario = scenarioRepository.findById(scenarioId).orElseThrow();
291+
Tag customTag = tagRepository.findByName(label).get();
292+
293+
assertThat(createdScenario.getTags()).contains(customTag);
294+
295+
List<Inject> injects =
296+
createdScenario.getInjects().stream()
297+
.filter(i -> i.getInjectorContract().get().getPayload() != null)
298+
.toList();
299+
assertThat(injects.size()).isEqualTo(1);
300+
301+
Inject inject = injects.getFirst();
302+
Set<AssetGroup> desiredAssetGroups =
303+
assetGroupService.fetchAssetGroupsFromScenarioTagRules(createdScenario);
304+
assertThat(inject.getAssetGroups())
305+
.containsExactlyInAnyOrderElementsOf(desiredAssetGroups.stream().toList());
306+
}
307+
183308
@Test
184309
@DisplayName("Should return 400 when STIX bundle has no security coverage")
185310
void shouldReturnBadRequestWhenNoSecurityCoverage() throws Exception {

openaev-api/src/test/java/io/openaev/utils/fixtures/TagRuleFixture.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ public static TagRule createTagRule(String tagRuleId, List<AssetGroup> assetGrou
5151
return rule;
5252
}
5353

54+
public static TagRule createDefaultTagRule() {
55+
return new TagRule();
56+
}
57+
5458
public static TagRuleOutput createTagRuleOutput() {
5559
return TagRuleOutput.builder()
5660
.tagName(TAG_NAME)

openaev-api/src/test/java/io/openaev/utils/fixtures/composers/PayloadComposer.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ public class Composer extends InnerComposerBase<Payload> {
2121
private final List<OutputParserComposer.Composer> outputParserComposers = new ArrayList<>();
2222
private final List<DetectionRemediationComposer.Composer> detectionRemediationComposers =
2323
new ArrayList<>();
24+
private final List<AttackPatternComposer.Composer> attackPatternComposers = new ArrayList<>();
2425

2526
public Composer(Payload payload) {
2627
this.payload = payload;
@@ -61,6 +62,14 @@ public Composer withExecutable(DocumentComposer.Composer newDocumentComposer) {
6162
return this;
6263
}
6364

65+
public Composer withAttackPattern(AttackPatternComposer.Composer attackPatternWrapper) {
66+
attackPatternComposers.add(attackPatternWrapper);
67+
List<AttackPattern> tempList = new ArrayList<>(payload.getAttackPatterns());
68+
tempList.add(attackPatternWrapper.get());
69+
payload.setAttackPatterns(tempList);
70+
return this;
71+
}
72+
6473
public Composer withOutputParser(OutputParserComposer.Composer outputParserComposer) {
6574
outputParserComposers.add(outputParserComposer);
6675
Set<OutputParser> outputParsers = payload.getOutputParsers();
@@ -74,6 +83,7 @@ public Composer persist() {
7483
documentComposer.ifPresent(DocumentComposer.Composer::persist);
7584
tagComposers.forEach(TagComposer.Composer::persist);
7685
detectionRemediationComposers.forEach(DetectionRemediationComposer.Composer::persist);
86+
attackPatternComposers.forEach(AttackPatternComposer.Composer::persist);
7787
payload.setId(null);
7888
payloadRepository.save(payload);
7989
return this;
@@ -85,6 +95,7 @@ public Composer delete() {
8595
tagComposers.forEach(TagComposer.Composer::delete);
8696
payloadRepository.delete(payload);
8797
detectionRemediationComposers.forEach(DetectionRemediationComposer.Composer::delete);
98+
attackPatternComposers.forEach(AttackPatternComposer.Composer::delete);
8899
return this;
89100
}
90101

0 commit comments

Comments
 (0)