From d9484e53033fd29a164fc569e1976679440dad53 Mon Sep 17 00:00:00 2001 From: AWSHurneyt Date: Wed, 3 Jul 2024 18:01:39 -0700 Subject: [PATCH 1/2] Refactored how STIX2IOCGenerator creates IOCs of specific types. Signed-off-by: AWSHurneyt --- .../util/STIX2IOCGenerator.java | 39 ++++++++++++------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/src/test/java/org/opensearch/securityanalytics/util/STIX2IOCGenerator.java b/src/test/java/org/opensearch/securityanalytics/util/STIX2IOCGenerator.java index a7c39bd72..8757305c2 100644 --- a/src/test/java/org/opensearch/securityanalytics/util/STIX2IOCGenerator.java +++ b/src/test/java/org/opensearch/securityanalytics/util/STIX2IOCGenerator.java @@ -7,12 +7,10 @@ import com.fasterxml.jackson.databind.ObjectMapper; import org.opensearch.common.xcontent.XContentFactory; -import org.opensearch.commons.alerting.model.Table; import org.opensearch.core.common.bytes.BytesReference; import org.opensearch.core.xcontent.ToXContent; import org.opensearch.core.xcontent.XContentBuilder; import org.opensearch.securityanalytics.SecurityAnalyticsPlugin; -import org.opensearch.securityanalytics.action.ListIOCsActionRequest; import org.opensearch.securityanalytics.commons.model.IOC; import org.opensearch.securityanalytics.commons.model.IOCType; import org.opensearch.securityanalytics.commons.utils.testUtils.PojoGenerator; @@ -24,6 +22,8 @@ import java.io.OutputStream; import java.io.PrintWriter; import java.time.Instant; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; import java.util.stream.IntStream; @@ -35,10 +35,8 @@ import static org.opensearch.test.OpenSearchTestCase.randomLong; public class STIX2IOCGenerator implements PojoGenerator { - List iocs; - - // Optional value. When not null, all IOCs generated will use this type. - IOCType type; + private List iocs; + private List types = Arrays.stream(IOCType.values()).collect(Collectors.toList()); private final ObjectMapper objectMapper; @@ -46,6 +44,11 @@ public STIX2IOCGenerator() { this.objectMapper = new ObjectMapper(); } + public STIX2IOCGenerator(List types) { + this(); + this.types = types; + } + @Override public void write(final int numberOfIOCs, final OutputStream outputStream) { try (final PrintWriter printWriter = new PrintWriter(outputStream)) { @@ -53,10 +56,20 @@ public void write(final int numberOfIOCs, final OutputStream outputStream) { } } + /** + * For each IOCType in 'types', 'numberOfIOCs' will be generated in the bucket object. + * Defaults to generating 'numberOfIOCs' of each IOCType. + * @param numberOfIOCs the number of each IOCType to generate in the bucket object. + * @param printWriter prints formatted representations of objects to a text-output stream. + */ private void writeLines(final int numberOfIOCs, final PrintWriter printWriter) { - final List iocs = IntStream.range(0, numberOfIOCs) - .mapToObj(i -> randomIOC(type)) - .collect(Collectors.toList()); + final List iocs = new ArrayList<>(); + for (IOCType type : types) { + final List newIocs = IntStream.range(0, numberOfIOCs) + .mapToObj(i -> randomIOC(type)) + .collect(Collectors.toList()); + iocs.addAll(newIocs); + } this.iocs = iocs; iocs.forEach(ioc -> writeLine(ioc, printWriter)); } @@ -101,12 +114,8 @@ public List getIocs() { return iocs; } - public IOCType getType() { - return type; - } - - public void setType(IOCType type) { - this.type = type; + public List getTypes() { + return types; } public static STIX2IOC randomIOC( From 51a1bebbd27351475354cfa1734f1d1bab6d8db3 Mon Sep 17 00:00:00 2001 From: AWSHurneyt Date: Wed, 3 Jul 2024 18:01:56 -0700 Subject: [PATCH 2/2] Added additional integration tests. Signed-off-by: AWSHurneyt --- .../SATIFSourceConfigRestApiIT.java | 493 +++++++++++++++--- 1 file changed, 434 insertions(+), 59 deletions(-) diff --git a/src/test/java/org/opensearch/securityanalytics/resthandler/SATIFSourceConfigRestApiIT.java b/src/test/java/org/opensearch/securityanalytics/resthandler/SATIFSourceConfigRestApiIT.java index 3abbf79d5..c30bd3cea 100644 --- a/src/test/java/org/opensearch/securityanalytics/resthandler/SATIFSourceConfigRestApiIT.java +++ b/src/test/java/org/opensearch/securityanalytics/resthandler/SATIFSourceConfigRestApiIT.java @@ -8,12 +8,12 @@ package org.opensearch.securityanalytics.resthandler; +import com.google.common.collect.ImmutableList; import org.junit.After; import org.junit.Assert; import org.junit.Before; -import org.junit.Ignore; -import org.junit.jupiter.api.condition.EnabledIfSystemProperty; import org.opensearch.client.Response; +import org.opensearch.client.ResponseException; import org.opensearch.jobscheduler.spi.schedule.IntervalSchedule; import org.opensearch.search.SearchHit; import org.opensearch.securityanalytics.SecurityAnalyticsPlugin; @@ -28,17 +28,27 @@ import org.opensearch.securityanalytics.threatIntel.model.SATIFSourceConfigDto; import org.opensearch.securityanalytics.threatIntel.model.Source; import org.opensearch.securityanalytics.util.STIX2IOCGenerator; +import software.amazon.awssdk.core.sync.RequestBody; import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; +import software.amazon.awssdk.services.s3.model.DeleteObjectResponse; +import software.amazon.awssdk.services.s3.model.HeadObjectRequest; +import software.amazon.awssdk.services.s3.model.NoSuchKeyException; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; +import software.amazon.awssdk.services.s3.model.PutObjectResponse; import java.io.IOException; import java.time.Instant; import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Objects; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @@ -61,8 +71,13 @@ * -Dtests.SATIFSourceConfigRestApiIT.bucketName= \ * -Dtests.SATIFSourceConfigRestApiIT.region= \ * -Dtests.SATIFSourceConfigRestApiIT.roleArn= + * + * Optionally, the following system parameter can be supplied to PREVENT the tests from cleaning up the bucket objects. + * This could be helpful when troubleshooting failing tests by investigating the data generated during execution. + * By default, the bucket objects (not the bucket) will be cleaned up after the tests. + * To disable cleanup, add the following system parameter. + * -Dtests.SATIFSourceConfigRestApiIT.cleanup=false */ -@EnabledIfSystemProperty(named = "tests.SATIFSourceConfigRestApiIT.bucketName", matches = ".+") public class SATIFSourceConfigRestApiIT extends SecurityAnalyticsRestTestCase { private String bucketName; @@ -74,17 +89,44 @@ public class SATIFSourceConfigRestApiIT extends SecurityAnalyticsRestTestCase { private S3ObjectGenerator s3ObjectGenerator; private STIX2IOCGenerator stix2IOCGenerator; + /** + * Is reassigned in the initSource function. + * Will only be TRUE if 'bucketName', 'region', and 'roleArn' are supplied through system params. + * Disables tests when FALSE. + */ + private boolean canRunTests; + + /** + * List of invalid type patterns for easy test execution + */ + private final List invalidTypes = ImmutableList.of( + "ip", // "ip" is not currently a supported IOCType + "ipv4-addr" // the IOCType enum currently uses underscores, not hyphens + ); + @Before public void initSource() { - // Retrieve system parameters needed to run the tests + // Retrieve system parameters needed to run the tests. Only retrieve once if (bucketName == null) { bucketName = System.getProperty("tests.SATIFSourceConfigRestApiIT.bucketName"); region = System.getProperty("tests.SATIFSourceConfigRestApiIT.region"); roleArn = System.getProperty("tests.SATIFSourceConfigRestApiIT.roleArn"); } + // Confirm necessary system params are provided + canRunTests = bucketName != null && !bucketName.isBlank() && + region != null && !region.isBlank() && + roleArn != null && !roleArn.isBlank(); + + // Exit test setup if necessary system params are not provided + if (!canRunTests) { + logger.info(getClass().getName() + " tests disabled."); + System.out.println(getClass().getName() + " tests disabled."); + return; + } + // Only create the s3Client once - if (bucketName != null && s3Client == null) { + if (s3Client == null) { s3Client = S3Client.builder() .region(Region.of(region)) .build(); @@ -98,15 +140,36 @@ public void initSource() { @After public void afterTest() { + // Exit test cleanup if necessary system params are not provided + if (!canRunTests) return; + + // Delete the bucket object unless cleanup is disabled + if (!Objects.equals(System.getProperty("tests.SATIFSourceConfigRestApiIT.cleanup"), "false")) { + DeleteObjectResponse response = s3Client.deleteObject( + DeleteObjectRequest.builder() + .bucket(bucketName) + .key(objectKey) + .build() + ); + + // Confirm bucket object was deleted successfully + assertTrue( + String.format("Failed to delete object with key %s in bucket %s", objectKey, bucketName), + response.sdkHttpResponse().isSuccessful() + ); + } + + // Close the client s3Client.close(); } - @Ignore public void testCreateSATIFSourceConfigAndVerifyJobRan() throws IOException, InterruptedException { + // Only run tests when required system params are provided + if (!canRunTests) return; + // Generate test IOCs, and upload them to S3 to create the bucket object. Feed creation fails if the bucket object doesn't exist. int numOfIOCs = 1; - stix2IOCGenerator = new STIX2IOCGenerator(); - stix2IOCGenerator.setType(IOCType.ipv4_addr); + stix2IOCGenerator = new STIX2IOCGenerator(List.of(IOCType.ipv4_addr)); s3ObjectGenerator.write(numOfIOCs, objectKey, stix2IOCGenerator); assertEquals("Incorrect number of test IOCs generated.", numOfIOCs, stix2IOCGenerator.getIocs().size()); @@ -115,7 +178,7 @@ public void testCreateSATIFSourceConfigAndVerifyJobRan() throws IOException, Int String feedFormat = "STIX2"; SourceConfigType sourceConfigType = SourceConfigType.S3_CUSTOM; IntervalSchedule schedule = new IntervalSchedule(Instant.now(), 1, ChronoUnit.MINUTES); - List iocTypes = List.of("ip", "domain-name"); + List iocTypes = List.of(IOCType.ipv4_addr.toString(), IOCType.domain_name.toString()); SATIFSourceConfigDto saTifSourceConfigDto = new SATIFSourceConfigDto( null, @@ -172,35 +235,13 @@ public void testCreateSATIFSourceConfigAndVerifyJobRan() throws IOException, Int }, 240, TimeUnit.SECONDS); } - /** - * Calls the get source config api and checks if the last updated time is different from the time that was passed in - * @param createdId - * @param firstUpdatedTime - * @return - * @throws IOException - */ - protected boolean verifyJobRan(String createdId, String firstUpdatedTime) throws IOException { - Response response; - Map responseBody; - - // call get API to get the latest source config by ID - response = makeRequest(client(), "GET", SecurityAnalyticsPlugin.THREAT_INTEL_SOURCE_URI + "/" + createdId, Collections.emptyMap(), null); - responseBody = asMap(response); - - String returnedLastUpdatedTime = (String) ((Map)responseBody.get("source_config")).get("last_update_time"); - - if(firstUpdatedTime.equals(returnedLastUpdatedTime.toString()) == false) { - return true; - } - return false; - } - - @Ignore public void testGetSATIFSourceConfigById() throws IOException { + // Only run tests when required system params are provided + if (!canRunTests) return; + // Generate test IOCs, and upload them to S3 to create the bucket object. Feed creation fails if the bucket object doesn't exist. int numOfIOCs = 1; - stix2IOCGenerator = new STIX2IOCGenerator(); - stix2IOCGenerator.setType(IOCType.hashes); + stix2IOCGenerator = new STIX2IOCGenerator(List.of(IOCType.hashes)); s3ObjectGenerator.write(numOfIOCs, objectKey, stix2IOCGenerator); assertEquals("Incorrect number of test IOCs generated.", numOfIOCs, stix2IOCGenerator.getIocs().size()); @@ -261,12 +302,13 @@ public void testGetSATIFSourceConfigById() throws IOException { Assert.assertTrue("Created ioc types and returned ioc types do not match", iocTypes.containsAll(returnedIocTypes) && returnedIocTypes.containsAll(iocTypes)); } - @Ignore public void testDeleteSATIFSourceConfig() throws IOException { + // Only run tests when required system params are provided + if (!canRunTests) return; + // Generate test IOCs, and upload them to S3 to create the bucket object. Feed creation fails if the bucket object doesn't exist. int numOfIOCs = 1; - stix2IOCGenerator = new STIX2IOCGenerator(); - stix2IOCGenerator.setType(IOCType.ipv4_addr); + stix2IOCGenerator = new STIX2IOCGenerator(List.of(IOCType.ipv4_addr)); s3ObjectGenerator.write(numOfIOCs, objectKey, stix2IOCGenerator); assertEquals("Incorrect number of test IOCs generated.", numOfIOCs, stix2IOCGenerator.getIocs().size()); @@ -275,7 +317,7 @@ public void testDeleteSATIFSourceConfig() throws IOException { String feedFormat = "STIX2"; SourceConfigType sourceConfigType = SourceConfigType.S3_CUSTOM; IntervalSchedule schedule = new IntervalSchedule(Instant.now(), 1, ChronoUnit.MINUTES); - List iocTypes = List.of("ip", "hashes"); + List iocTypes = List.of(IOCType.ipv4_addr.toString(), IOCType.hashes.toString()); SATIFSourceConfigDto saTifSourceConfigDto = new SATIFSourceConfigDto( null, @@ -330,21 +372,121 @@ public void testDeleteSATIFSourceConfig() throws IOException { Assert.assertEquals(0, hits.size()); } - @Ignore public void testRetrieveIOCsSuccessfully() throws IOException, InterruptedException { - // Generate test IOCs, and upload them to S3 + // Only run tests when required system params are provided + if (!canRunTests) return; + + // Execute test for each IOCType + for (IOCType type : IOCType.values()) { + // Generate test IOCs, and upload them to S3 + int numOfIOCs = 5; + stix2IOCGenerator = new STIX2IOCGenerator(List.of(type)); + s3ObjectGenerator.write(numOfIOCs, objectKey, stix2IOCGenerator); + assertEquals("Incorrect number of test IOCs generated for type: " + type, numOfIOCs, stix2IOCGenerator.getIocs().size()); + + // Create test feed + String feedName = "download_test_feed_name"; + String feedFormat = "STIX2"; + SourceConfigType sourceConfigType = SourceConfigType.S3_CUSTOM; + IntervalSchedule schedule = new IntervalSchedule(Instant.now(), 1, ChronoUnit.MINUTES); + List iocTypes = List.of(type.toString()); + + SATIFSourceConfigDto saTifSourceConfigDto = new SATIFSourceConfigDto( + null, + null, + feedName, + feedFormat, + sourceConfigType, + null, + null, + Instant.now(), + source, + null, + Instant.now(), + schedule, + null, + null, + Instant.now(), + null, + true, + iocTypes + ); + + // Confirm test feed was created successfully + Response response = makeRequest(client(), "POST", SecurityAnalyticsPlugin.THREAT_INTEL_SOURCE_URI, Collections.emptyMap(), toHttpEntity(saTifSourceConfigDto)); + Assert.assertEquals(201, response.getStatusLine().getStatusCode()); + Map responseBody = asMap(response); + + String createdId = responseBody.get("_id").toString(); + Assert.assertNotEquals("Response is missing Id", SATIFSourceConfigDto.NO_ID, createdId); + + + // Wait for feed to execute + String firstUpdatedTime = (String) ((Map)responseBody.get("source_config")).get("last_refreshed_time"); + waitUntil(() -> { + try { + return verifyJobRan(createdId, firstUpdatedTime); + } catch (IOException e) { + throw new RuntimeException("failed to verify that job ran"); + } + }, 240, TimeUnit.SECONDS); + + // Confirm IOCs were ingested to system index for the feed + String indexName = STIX2IOCFeedStore.getIocIndexAlias(createdId); + String request = "{\n" + + " \"query\" : {\n" + + " \"match_all\":{\n" + + " }\n" + + " }\n" + + "}"; + List hits = executeSearch(indexName, request); + + // Confirm expected number of results are returned + assertEquals(numOfIOCs, hits.size()); + List> iocs = hits.stream() + .map(SearchHit::getSourceAsMap) + .collect(Collectors.toList()); + + // Sort IOC lists for easy comparison + stix2IOCGenerator.getIocs().sort(Comparator.comparing(STIX2IOC::getName)); + iocs.sort(Comparator.comparing(ioc -> (String) ioc.get(STIX2IOC.NAME_FIELD))); + + // Confirm expected IOCs have been ingested + for (int i = 0; i < numOfIOCs; i++) { + assertEquals(stix2IOCGenerator.getIocs().get(i).getName(), iocs.get(i).get(STIX2IOC.NAME_FIELD)); + assertEquals(stix2IOCGenerator.getIocs().get(i).getType(), IOCType.fromString((String) iocs.get(i).get(STIX2IOC.TYPE_FIELD))); + assertEquals(stix2IOCGenerator.getIocs().get(i).getValue(), iocs.get(i).get(STIX2IOC.VALUE_FIELD)); + assertEquals(stix2IOCGenerator.getIocs().get(i).getSeverity(), iocs.get(i).get(STIX2IOC.SEVERITY_FIELD)); + + // TODO troubleshoot instant assertions +// assertEquals(stix2IOCGenerator.getIocs().get(i).getCreated().toString(), iocs.get(i).get(STIX2IOC.CREATED_FIELD)); +// assertEquals(stix2IOCGenerator.getIocs().get(i).getModified().toString(), iocs.get(i).get(STIX2IOC.MODIFIED_FIELD)); + + assertEquals(stix2IOCGenerator.getIocs().get(i).getDescription(), iocs.get(i).get(STIX2IOC.DESCRIPTION_FIELD)); + assertEquals(stix2IOCGenerator.getIocs().get(i).getLabels(), iocs.get(i).get(STIX2IOC.LABELS_FIELD)); + assertEquals(createdId, iocs.get(i).get(STIX2IOC.FEED_ID_FIELD)); + assertEquals(stix2IOCGenerator.getIocs().get(i).getSpecVersion(), iocs.get(i).get(STIX2IOC.SPEC_VERSION_FIELD)); + } + } + } + + public void testRetrieveMultipleIOCTypesSuccessfully() throws IOException, InterruptedException { + // Only run tests when required system params are provided + if (!canRunTests) return; + + // Generate test IOCs for each type, and upload them to S3 int numOfIOCs = 5; stix2IOCGenerator = new STIX2IOCGenerator(); - stix2IOCGenerator.setType(IOCType.ipv4_addr); s3ObjectGenerator.write(numOfIOCs, objectKey, stix2IOCGenerator); - assertEquals("Incorrect number of test IOCs generated.", numOfIOCs, stix2IOCGenerator.getIocs().size()); + List allIocs = stix2IOCGenerator.getIocs(); + assertEquals("Incorrect total number of test IOCs generated.", IOCType.values().length * numOfIOCs, allIocs.size()); // Create test feed String feedName = "download_test_feed_name"; String feedFormat = "STIX2"; SourceConfigType sourceConfigType = SourceConfigType.S3_CUSTOM; IntervalSchedule schedule = new IntervalSchedule(Instant.now(), 1, ChronoUnit.MINUTES); - List iocTypes = List.of(IOCType.ipv4_addr.toString()); + List iocTypes = Arrays.stream(IOCType.values()).map(Enum::toString).collect(Collectors.toList()); SATIFSourceConfigDto saTifSourceConfigDto = new SATIFSourceConfigDto( null, @@ -388,7 +530,10 @@ public void testRetrieveIOCsSuccessfully() throws IOException, InterruptedExcept // Confirm IOCs were ingested to system index for the feed String indexName = STIX2IOCFeedStore.getIocIndexAlias(createdId); + logger.info("hurneyt indexName = {}", indexName); + String request = "{\n" + + " \"size\" : 10000,\n" + " \"query\" : {\n" + " \"match_all\":{\n" + " }\n" + @@ -397,30 +542,260 @@ public void testRetrieveIOCsSuccessfully() throws IOException, InterruptedExcept List hits = executeSearch(indexName, request); // Confirm expected number of results are returned - assertEquals(numOfIOCs, hits.size()); - List> iocs = hits.stream() + assertEquals(allIocs.size(), hits.size()); + List> iocHits = hits.stream() .map(SearchHit::getSourceAsMap) .collect(Collectors.toList()); // Sort IOC lists for easy comparison - stix2IOCGenerator.getIocs().sort(Comparator.comparing(STIX2IOC::getName)); - iocs.sort(Comparator.comparing(ioc -> (String) ioc.get(STIX2IOC.NAME_FIELD))); + allIocs.sort(Comparator.comparing(STIX2IOC::getName)); + iocHits.sort(Comparator.comparing(ioc -> (String) ioc.get(STIX2IOC.NAME_FIELD))); // Confirm expected IOCs have been ingested - for (int i = 0; i < numOfIOCs; i++) { - assertEquals(stix2IOCGenerator.getIocs().get(i).getName(), iocs.get(i).get(STIX2IOC.NAME_FIELD)); - assertEquals(stix2IOCGenerator.getIocs().get(i).getType(), IOCType.fromString((String) iocs.get(i).get(STIX2IOC.TYPE_FIELD))); - assertEquals(stix2IOCGenerator.getIocs().get(i).getValue(), iocs.get(i).get(STIX2IOC.VALUE_FIELD)); - assertEquals(stix2IOCGenerator.getIocs().get(i).getSeverity(), iocs.get(i).get(STIX2IOC.SEVERITY_FIELD)); + for (int i = 0; i < allIocs.size(); i++) { + assertEquals(stix2IOCGenerator.getIocs().get(i).getName(), iocHits.get(i).get(STIX2IOC.NAME_FIELD)); + assertEquals(stix2IOCGenerator.getIocs().get(i).getType(), IOCType.fromString((String) iocHits.get(i).get(STIX2IOC.TYPE_FIELD))); + assertEquals(stix2IOCGenerator.getIocs().get(i).getValue(), iocHits.get(i).get(STIX2IOC.VALUE_FIELD)); + assertEquals(stix2IOCGenerator.getIocs().get(i).getSeverity(), iocHits.get(i).get(STIX2IOC.SEVERITY_FIELD)); // TODO troubleshoot instant assertions -// assertEquals(stix2IOCGenerator.getIocs().get(i).getCreated().toString(), iocs.get(i).get(STIX2IOC.CREATED_FIELD)); -// assertEquals(stix2IOCGenerator.getIocs().get(i).getModified().toString(), iocs.get(i).get(STIX2IOC.MODIFIED_FIELD)); +// assertEquals(stix2IOCGenerator.getIocs().get(i).getCreated().toString(), iocHits.get(i).get(STIX2IOC.CREATED_FIELD)); +// assertEquals(stix2IOCGenerator.getIocs().get(i).getModified().toString(), iocHits.get(i).get(STIX2IOC.MODIFIED_FIELD)); + + assertEquals(stix2IOCGenerator.getIocs().get(i).getDescription(), iocHits.get(i).get(STIX2IOC.DESCRIPTION_FIELD)); + assertEquals(stix2IOCGenerator.getIocs().get(i).getLabels(), iocHits.get(i).get(STIX2IOC.LABELS_FIELD)); + assertEquals(createdId, iocHits.get(i).get(STIX2IOC.FEED_ID_FIELD)); + assertEquals(stix2IOCGenerator.getIocs().get(i).getSpecVersion(), iocHits.get(i).get(STIX2IOC.SPEC_VERSION_FIELD)); + } + } + + public void testWithValidAndInvalidIOCTypes() throws IOException { + // Only run tests when required system params are provided + if (!canRunTests) return; + + // Generate test IOCs, and upload them to S3 + int numOfIOCs = 5; + stix2IOCGenerator = new STIX2IOCGenerator(List.of(IOCType.ipv4_addr)); + s3ObjectGenerator.write(numOfIOCs, objectKey, stix2IOCGenerator); + assertEquals("Incorrect number of test IOCs generated.", numOfIOCs, stix2IOCGenerator.getIocs().size()); + + List types = new ArrayList<>(invalidTypes); + types.addAll(Arrays.stream(IOCType.values()).map(Enum::toString).collect(Collectors.toList())); + + // Execute the test for each invalid type + for (String type : invalidTypes) { + // Create test feed + String feedName = "download_test_feed_name"; + String feedFormat = "STIX2"; + SourceConfigType sourceConfigType = SourceConfigType.S3_CUSTOM; + IntervalSchedule schedule = new IntervalSchedule(Instant.now(), 1, ChronoUnit.MINUTES); + + List iocTypes = List.of(type); + + SATIFSourceConfigDto saTifSourceConfigDto = new SATIFSourceConfigDto( + null, + null, + feedName, + feedFormat, + sourceConfigType, + null, + null, + Instant.now(), + source, + null, + Instant.now(), + schedule, + null, + null, + Instant.now(), + null, + true, + iocTypes + ); + + Exception exception = assertThrows(ResponseException.class, () -> + makeRequest(client(), "POST", SecurityAnalyticsPlugin.THREAT_INTEL_SOURCE_URI, Collections.emptyMap(), toHttpEntity(saTifSourceConfigDto)) + ); + + String expectedError = "{\"error\":{\"root_cause\":[{\"type\":\"status_exception\",\"reason\":\"No compatible Iocs were downloaded for config download_test_feed_name\"}],\"type\":\"status_exception\",\"reason\":\"No compatible Iocs were downloaded for config download_test_feed_name\"},\"status\":400}"; + assertTrue(exception.getMessage().contains(expectedError)); + } + } + + public void testWithInvalidIOCTypes() throws IOException { + // Only run tests when required system params are provided + if (!canRunTests) return; + + // Generate test IOCs, and upload them to S3 + int numOfIOCs = 5; + stix2IOCGenerator = new STIX2IOCGenerator(List.of(IOCType.ipv4_addr)); + s3ObjectGenerator.write(numOfIOCs, objectKey, stix2IOCGenerator); + assertEquals("Incorrect number of test IOCs generated.", numOfIOCs, stix2IOCGenerator.getIocs().size()); + + // Execute the test for each invalid type + for (String type : invalidTypes) { + // Create test feed + String feedName = "download_test_feed_name"; + String feedFormat = "STIX2"; + SourceConfigType sourceConfigType = SourceConfigType.S3_CUSTOM; + IntervalSchedule schedule = new IntervalSchedule(Instant.now(), 1, ChronoUnit.MINUTES); + + List iocTypes = List.of(type); + + SATIFSourceConfigDto saTifSourceConfigDto = new SATIFSourceConfigDto( + null, + null, + feedName, + feedFormat, + sourceConfigType, + null, + null, + Instant.now(), + source, + null, + Instant.now(), + schedule, + null, + null, + Instant.now(), + null, + true, + iocTypes + ); + + Exception exception = assertThrows(ResponseException.class, () -> + makeRequest(client(), "POST", SecurityAnalyticsPlugin.THREAT_INTEL_SOURCE_URI, Collections.emptyMap(), toHttpEntity(saTifSourceConfigDto)) + ); + + String expectedError = "{\"error\":{\"root_cause\":[{\"type\":\"status_exception\",\"reason\":\"No compatible Iocs were downloaded for config download_test_feed_name\"}],\"type\":\"status_exception\",\"reason\":\"No compatible Iocs were downloaded for config download_test_feed_name\"},\"status\":400}"; + assertTrue(exception.getMessage().contains(expectedError)); + } + } + + public void testWithNoIOCsToDownload() { + // Only run tests when required system params are provided + if (!canRunTests) return; + + // Create the bucket object without any IOCs + PutObjectRequest putObjectRequest = PutObjectRequest.builder() + .bucket(bucketName) + .key(objectKey) + .build(); + PutObjectResponse putObjectResponse = s3Client.putObject(putObjectRequest, RequestBody.empty()); + assertTrue("Failed to create empty bucket object for type.", putObjectResponse.sdkHttpResponse().isSuccessful()); + + // Execute the test case for each IOC type + for (IOCType type : IOCType.values()) { + // Create test feed + String feedName = "download_test_feed_name"; + String feedFormat = "STIX2"; + SourceConfigType sourceConfigType = SourceConfigType.S3_CUSTOM; + IntervalSchedule schedule = new IntervalSchedule(Instant.now(), 1, ChronoUnit.MINUTES); + List iocTypes = List.of(type.toString()); + + SATIFSourceConfigDto saTifSourceConfigDto = new SATIFSourceConfigDto( + null, + null, + feedName, + feedFormat, + sourceConfigType, + null, + null, + Instant.now(), + source, + null, + Instant.now(), + schedule, + null, + null, + Instant.now(), + null, + true, + iocTypes + ); + + Exception exception = assertThrows(ResponseException.class, () -> + makeRequest(client(), "POST", SecurityAnalyticsPlugin.THREAT_INTEL_SOURCE_URI, Collections.emptyMap(), toHttpEntity(saTifSourceConfigDto)) + ); + + String expectedError = "{\"error\":{\"root_cause\":[{\"type\":\"status_exception\",\"reason\":\"No compatible Iocs were downloaded for config download_test_feed_name\"}],\"type\":\"status_exception\",\"reason\":\"No compatible Iocs were downloaded for config download_test_feed_name\"},\"status\":400}"; + assertTrue(exception.getMessage().contains(expectedError)); + } + } + + public void testWhenBucketObjectDoesNotExist() { + // Only run tests when required system params are provided + if (!canRunTests) return; + + // Confirm bucket object does not exist + HeadObjectRequest headObjectRequest = HeadObjectRequest.builder() + .bucket(bucketName) + .key(objectKey) + .build(); + assertThrows( + String.format("Object %s in bucket %s should not exist.", objectKey, bucketName), + NoSuchKeyException.class, () -> s3Client.headObject(headObjectRequest) + ); - assertEquals(stix2IOCGenerator.getIocs().get(i).getDescription(), iocs.get(i).get(STIX2IOC.DESCRIPTION_FIELD)); - assertEquals(stix2IOCGenerator.getIocs().get(i).getLabels(), iocs.get(i).get(STIX2IOC.LABELS_FIELD)); - assertEquals(createdId, iocs.get(i).get(STIX2IOC.FEED_ID_FIELD)); - assertEquals(stix2IOCGenerator.getIocs().get(i).getSpecVersion(), iocs.get(i).get(STIX2IOC.SPEC_VERSION_FIELD)); + // Execute the test case for each IOC type + for (IOCType type : IOCType.values()) { + // Create test feed + String feedName = "download_test_feed_name"; + String feedFormat = "STIX2"; + SourceConfigType sourceConfigType = SourceConfigType.S3_CUSTOM; + IntervalSchedule schedule = new IntervalSchedule(Instant.now(), 1, ChronoUnit.MINUTES); + List iocTypes = List.of(type.toString()); + + SATIFSourceConfigDto saTifSourceConfigDto = new SATIFSourceConfigDto( + null, + null, + feedName, + feedFormat, + sourceConfigType, + null, + null, + Instant.now(), + source, + null, + Instant.now(), + schedule, + null, + null, + Instant.now(), + null, + true, + iocTypes + ); + + Exception exception = assertThrows(ResponseException.class, () -> + makeRequest(client(), "POST", SecurityAnalyticsPlugin.THREAT_INTEL_SOURCE_URI, Collections.emptyMap(), toHttpEntity(saTifSourceConfigDto)) + ); + + String expectedError = "{\"error\":{\"root_cause\":[{\"type\":\"no_such_key_exception\",\"reason\":\"The specified key does not exist."; + assertTrue("Exception contains unexpected message: " + exception.getMessage(), exception.getMessage().contains(expectedError)); } } + + /** + * Calls the get source config api and checks if the last updated time is different from the time that was passed in + * @param createdId + * @param firstUpdatedTime + * @return + * @throws IOException + */ + protected boolean verifyJobRan(String createdId, String firstUpdatedTime) throws IOException { + Response response; + Map responseBody; + + // call get API to get the latest source config by ID + response = makeRequest(client(), "GET", SecurityAnalyticsPlugin.THREAT_INTEL_SOURCE_URI + "/" + createdId, Collections.emptyMap(), null); + responseBody = asMap(response); + + String returnedLastUpdatedTime = (String) ((Map) responseBody.get("source_config")).get("last_update_time"); + + if(firstUpdatedTime.equals(returnedLastUpdatedTime.toString()) == false) { + return true; + } + return false; + } }