EQL: Test infrastructure improvements (#53253) (#53297)

Update CommonEqlRestTestCase code to simplify making changes as requested.
Update EqlActionIT to simplify the test code as requested.
Replace Jackson parser with XContent in EqlActionIT.
Whitelist more EQL tests specs that are now supported.
This commit is contained in:
Aleksandr Maus 2020-03-09 14:11:54 -04:00 committed by GitHub
parent c33afea9fb
commit d064846416
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 59 additions and 199 deletions

View File

@ -16,53 +16,28 @@ import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass;
import java.util.ArrayList;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is;
public abstract class CommonEqlRestTestCase extends ESRestTestCase {
static class SearchTestConfiguration {
final String input;
final int expectedStatus;
final String expectedMessage;
SearchTestConfiguration(String input, int status, String msg) {
this.input = input;
this.expectedStatus = status;
this.expectedMessage = msg;
}
}
public static final String defaultValidationIndexName = "eql_search_validation_test";
private static final String defaultValidationIndexName = "eql_search_validation_test";
private static final String validQuery = "process where user = 'SYSTEM'";
public static final ArrayList<SearchTestConfiguration> searchValidationTests;
static {
searchValidationTests = new ArrayList<>();
searchValidationTests.add(new SearchTestConfiguration(null, 400, "request body or source parameter is required"));
searchValidationTests.add(new SearchTestConfiguration("{}", 400, "query is null or empty"));
searchValidationTests.add(new SearchTestConfiguration("{\"query\": \"\"}", 400, "query is null or empty"));
searchValidationTests.add(new SearchTestConfiguration("{\"query\": \"" + validQuery + "\", \"timestamp_field\": \"\"}",
400, "timestamp field is null or empty"));
searchValidationTests.add(new SearchTestConfiguration("{\"query\": \"" + validQuery + "\", \"event_category_field\": \"\"}",
400, "event category field is null or empty"));
searchValidationTests.add(new SearchTestConfiguration("{\"query\": \"" + validQuery + "\", \"implicit_join_key_field\": \"\"}",
400, "implicit join key field is null or empty"));
searchValidationTests.add(new SearchTestConfiguration("{\"query\": \"" + validQuery + "\", \"size\": 0}",
400, "size must be greater than 0"));
searchValidationTests.add(new SearchTestConfiguration("{\"query\": \"" + validQuery + "\", \"size\": -1}",
400, "size must be greater than 0"));
searchValidationTests.add(new SearchTestConfiguration("{\"query\": \"" + validQuery + "\", \"search_after\": null}",
400, "search_after doesn't support values of type: VALUE_NULL"));
searchValidationTests.add(new SearchTestConfiguration("{\"query\": \"" + validQuery + "\", \"search_after\": []}",
400, "must contains at least one value"));
searchValidationTests.add(new SearchTestConfiguration("{\"query\": \"" + validQuery + "\", \"filter\": null}",
400, "filter doesn't support values of type: VALUE_NULL"));
searchValidationTests.add(new SearchTestConfiguration("{\"query\": \"" + validQuery + "\", \"filter\": {}}",
400, "query malformed, empty clause found"));
}
private static final String[][] testBadRequests = {
{null, "request body or source parameter is required"},
{"{}", "query is null or empty"},
{"{\"query\": \"\"}", "query is null or empty"},
{"{\"query\": \"" + validQuery + "\", \"timestamp_field\": \"\"}", "timestamp field is null or empty"},
{"{\"query\": \"" + validQuery + "\", \"event_category_field\": \"\"}", "event category field is null or empty"},
{"{\"query\": \"" + validQuery + "\", \"implicit_join_key_field\": \"\"}", "implicit join key field is null or empty"},
{"{\"query\": \"" + validQuery + "\", \"size\": 0}", "size must be greater than 0"},
{"{\"query\": \"" + validQuery + "\", \"size\": -1}", "size must be greater than 0"},
{"{\"query\": \"" + validQuery + "\", \"search_after\": null}", "search_after doesn't support values of type: VALUE_NULL"},
{"{\"query\": \"" + validQuery + "\", \"search_after\": []}", "must contains at least one value"},
{"{\"query\": \"" + validQuery + "\", \"filter\": null}", "filter doesn't support values of type: VALUE_NULL"},
{"{\"query\": \"" + validQuery + "\", \"filter\": {}}", "query malformed, empty clause found"}
};
@BeforeClass
public static void checkForSnapshot() {
@ -79,24 +54,19 @@ public abstract class CommonEqlRestTestCase extends ESRestTestCase {
deleteIndex(defaultValidationIndexName);
}
public void testSearchValidationFailures() throws Exception {
public void testBadRequests() throws Exception {
final String contentType = "application/json";
for (SearchTestConfiguration config : searchValidationTests) {
for (String[] test : testBadRequests) {
final String endpoint = "/" + defaultValidationIndexName + "/_eql/search";
Request request = new Request("GET", endpoint);
request.setJsonEntity(config.input);
request.setJsonEntity(test[0]);
Response response = null;
if (config.expectedStatus == 400) {
ResponseException e = expectThrows(ResponseException.class, () -> client().performRequest(request));
response = e.getResponse();
} else {
response = client().performRequest(request);
}
ResponseException e = expectThrows(ResponseException.class, () -> client().performRequest(request));
Response response = e.getResponse();
assertThat(response.getHeader("Content-Type"), containsString(contentType));
assertThat(EntityUtils.toString(response.getEntity()), containsString(config.expectedMessage));
assertThat(response.getStatusLine().getStatusCode(), is(config.expectedStatus));
assertThat(EntityUtils.toString(response.getEntity()), containsString(test[1]));
assertThat(response.getStatusLine().getStatusCode(), is(400));
}
}
}

View File

@ -7,33 +7,32 @@
package org.elasticsearch.xpack.eql.action;
import com.carrotsearch.randomizedtesting.annotations.ParametersFactory;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.elasticsearch.Build;
import org.elasticsearch.action.bulk.BulkRequestBuilder;
import org.elasticsearch.action.bulk.BulkResponse;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.support.WriteRequest;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.common.xcontent.json.JsonXContent;
import org.elasticsearch.search.SearchHit;
import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.HashMap;
import java.util.List;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.instanceOf;
public class EqlActionIT extends AbstractEqlIntegTestCase {
static final String indexPrefix = "endgame";
static final String testIndexName = indexPrefix + "-1.4.0";
protected static final String PARAM_FORMATTING = "%1$s.test";
protected static final String PARAM_FORMATTING = "%1$s.test -> %2$s";
@BeforeClass
public static void checkForSnapshot() {
@ -41,26 +40,29 @@ public class EqlActionIT extends AbstractEqlIntegTestCase {
}
@Before
@SuppressWarnings("unchecked")
public void setUpData() throws Exception {
// Insert test data
ObjectMapper mapper = new ObjectMapper();
BulkRequestBuilder bulkBuilder = client().prepareBulk();
JsonNode rootNode = mapper.readTree(EqlActionIT.class.getResourceAsStream("/test_data.json"));
Iterator<JsonNode> entries = rootNode.elements();
while (entries.hasNext()) {
JsonNode entry = entries.next();
try (XContentParser parser = createParser(JsonXContent.jsonXContent, EqlActionIT.class.getResourceAsStream("/test_data.json"))) {
List<Object> list = parser.list();
for (Object item : list) {
assertThat(item, instanceOf(HashMap.class));
// Adjust the structure of the document with additional event.category and @timestamp fields
// Add event.category field
ObjectNode objEvent = ((ObjectNode)entry).putObject("event");
JsonNode objEventType = entry.get("event_type");
objEvent.put("category", objEventType.asText());
HashMap<String, Object> entry = (HashMap<String, Object>) item;
// Add @timestamp field
JsonNode objTimestamp = entry.get("timestamp");
((ObjectNode)entry).put("@timestamp", objTimestamp.asLong());
// Adjust the structure of the document with additional event.category and @timestamp fields
// Add event.category field
HashMap<String, Object> objEvent = new HashMap<>();
objEvent.put("category", entry.get("event_type"));
entry.put("event", objEvent);
bulkBuilder.add(new IndexRequest(testIndexName).source(entry.toString(), XContentType.JSON));
// Add @timestamp field
entry.put("@timestamp", entry.get("timestamp"));
bulkBuilder.add(new IndexRequest(testIndexName).source(entry, XContentType.JSON));
}
}
BulkResponse bulkResponse = bulkBuilder.setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE).get();
assertThat(bulkResponse.hasFailures() ? bulkResponse.buildFailureMessage() : "", bulkResponse.hasFailures(), equalTo(false));
@ -108,19 +110,24 @@ public class EqlActionIT extends AbstractEqlIntegTestCase {
this.spec = spec;
}
public final void test() {
EqlSearchResponse response = new EqlSearchRequestBuilder(client(), EqlSearchAction.INSTANCE)
.indices(testIndexName).query(spec.query()).get();
List<SearchHit> events = response.hits().events();
assertNotNull(events);
private static long[] extractIds(List<SearchHit> events) {
final int len = events.size();
final long ids[] = new long[len];
for (int i = 0; i < events.size(); i++) {
for (int i = 0; i < len; i++) {
ids[i] = ((Number) events.get(i).getSourceAsMap().get("serial_event_id")).longValue();
}
final String msg = "unexpected result for spec: [" + spec.toString() + "]";
assertArrayEquals(msg, spec.expectedEventIds(), ids);
return ids;
}
private void assertSpec(List<SearchHit> events) {
assertNotNull(events);
assertArrayEquals("unexpected result for spec: [" + spec.toString() + "]", spec.expectedEventIds(), extractIds(events));
}
public final void test() {
EqlSearchResponse response = new EqlSearchRequestBuilder(client(), EqlSearchAction.INSTANCE)
.indices(testIndexName).query(spec.query()).get();
assertSpec(response.hits().events());
}
}

View File

@ -12,14 +12,11 @@
# query = 'process where serial_event_id = 1'
# expected_event_ids = [1]
[[queries]]
query = 'process where serial_event_id < 4'
expected_event_ids = [1, 2, 3]
[[queries]]
query = 'process where true | head 6'
expected_event_ids = [1, 2, 3, 4, 5, 6]
# presently not supported, throwing: org.elasticsearch.xpack.ql.rule.RuleExecutionException: Does not know how to handle a local relation
[[queries]]
query = 'process where false'
expected_event_ids = []
@ -32,12 +29,6 @@ query = 'process where missing_field != null'
expected_event_ids = [1, 2, 3, 4, 5]
query = 'process where bad_field == null | head 5'
[[queries]]
query = '''
process where process_name == "impossible name" or (serial_event_id < 4.5 and serial_event_id >= 3.1)
'''
expected_event_ids = [4]
[[queries]]
tags = ["comparisons", "pipes"]
query = '''
@ -70,42 +61,6 @@ process where true
'''
expected_event_ids = [9, 10]
[[queries]]
query = '''
process where serial_event_id<=8 and serial_event_id > 7
'''
expected_event_ids = [8]
[[queries]]
note = "check that comparisons against null values return false"
expected_event_ids = [58, 64, 69, 74, 80, 85, 90, 93, 94, 75303]
query = 'process where exit_code >= 0'
[[queries]]
note = "check that comparisons against null values return false"
expected_event_ids = [58, 64, 69, 74, 80, 85, 90, 93, 94, 75303]
query = 'process where 0 <= exit_code'
[[queries]]
note = "check that comparisons against null values return false"
expected_event_ids = [58, 64, 69, 74, 80, 85, 90, 93, 94, 75303]
query = 'process where exit_code <= 0'
[[queries]]
note = "check that comparisons against null values return false"
expected_event_ids = [58, 64, 69, 74, 80, 85, 90, 93, 94, 75303]
query = 'process where exit_code < 1'
[[queries]]
note = "check that comparisons against null values return false"
expected_event_ids = [58, 64, 69, 74, 80, 85, 90, 93, 94, 75303]
query = 'process where exit_code > -1'
[[queries]]
note = "check that comparisons against null values return false"
expected_event_ids = [58, 64, 69, 74, 80, 85, 90, 93, 94, 75303]
query = 'process where -1 < exit_code'
[[queries]]
note = "check that comparisons against null values return false"
expected_event_ids = []
@ -125,26 +80,6 @@ note = "check that comparisons against null values return false"
expected_event_ids = [1, 2, 3, 4, 5, 6, 7]
query = 'process where not (-1 < exit_code) | head 7'
[[queries]]
query = 'process where exit_code > 0'
expected_event_ids = []
[[queries]]
query = 'process where exit_code < 0'
expected_event_ids = []
[[queries]]
query = 'process where 0 < exit_code'
expected_event_ids = []
[[queries]]
query = 'process where 0 > exit_code'
expected_event_ids = []
[[queries]]
query = 'process where (serial_event_id<=8 and serial_event_id > 7) and (opcode=3 and opcode>2)'
expected_event_ids = [8]
[[queries]]
query = 'process where (serial_event_id<9 and serial_event_id >= 7) or (opcode == pid)'
expected_event_ids = [7, 8]
@ -217,16 +152,6 @@ expected_event_ids = [57]
query = '''
registry where length(bytes_written_string_list) == 2 and bytes_written_string_list[1] == "EN"'''
[[queries]]
query = '''
registry where key_path == "*\\MACHINE\\SAM\\SAM\\*\\Account\\Us*ers\\00*03E9\\F"'''
expected_event_ids = [79]
[[queries]]
query = '''
process where process_path == "*\\red_ttp\\wininit.*" and opcode in (0,1,2,3,4)'''
expected_event_ids = [84, 85]
[[queries]]
query = '''
file where file_name == "csrss.exe" and opcode=0
@ -253,34 +178,6 @@ process where opcode=1 and process_name == "smss.exe"
'''
expected_event_ids = [78]
[[queries]]
query = '''
file where file_path="*\\red_ttp\\winin*.*"
and opcode in (0,1,2) and user_name="vagrant"
'''
expected_event_ids = [83, 86]
[[queries]]
query = '''
file where file_path="*\\red_ttp\\winin*.*"
and opcode not in (0,1,2) and user_name="vagrant"
'''
expected_event_ids = []
[[queries]]
query = '''
file where file_path="*\\red_ttp\\winin*.*"
and opcode not in (3, 4, 5, 6 ,7) and user_name="vagrant"
'''
expected_event_ids = [83, 86]
[[queries]]
query = '''
file where file_name in ("wininit.exe", "lsass.exe") and opcode == 2
'''
expected_event_ids = [65, 86]
[[queries]]
query = '''
file where true
@ -612,15 +509,6 @@ query = '''
registry where length(bad_field) > 0
'''
[[queries]]
query = '''
process where opcode == 1
and process_name in ("net.exe", "net1.exe")
and not (parent_process_name == "net.exe"
and process_name == "net1.exe")
and command_line == "*group *admin*" and command_line != "* /add*"'''
expected_event_ids = [97]
[[queries]]
expected_event_ids = [1, 55, 57, 63, 75304]
query = '''
@ -690,11 +578,6 @@ file where event of [process where process_name = "python.exe" ]
| unique unique_pid'''
expected_event_ids = [55, 95]
[[queries]]
query = '''
process where process_name = "python.exe"'''
expected_event_ids = [48, 50, 51, 54, 93]
[[queries]]
query = 'process where event of [process where process_name = "python.exe" ]'
expected_event_ids = [48, 50, 51, 54, 93]