EQL: case sensitivity aware integration testing (#58624) (#58672)

* EQL: case sensitivity aware integration testing (#58624)

* Add DataLoader
* Rewrite case sensitivity settings:
NULL -> run both case sensitive and insensitive tests
TRUE -> run case sensitive test only
FALSE -> run case insensitive test only
* Rename test_queries_supported
* Add more toml tests from the Python client

Co-authored-by: Ross Wolf <31489089+rw-access@users.noreply.github.com>
(cherry picked from commit 34d383421599f060a5c083b40df35f135de49e39)
This commit is contained in:
Andrei Stefan 2020-06-29 18:40:07 +03:00 committed by GitHub
parent be20aacec3
commit 3cb8f54f28
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 1282 additions and 491 deletions

View File

@ -41,6 +41,8 @@ public class EqlSearchRequest implements Validatable, ToXContentObject {
private String timestampField = "@timestamp";
private String eventCategoryField = "event.category";
private String implicitJoinKeyField = "agent.id";
private boolean isCaseSensitive = true;
private int fetchSize = 50;
private SearchAfterBuilder searchAfterBuilder;
private String query;
@ -56,6 +58,7 @@ public class EqlSearchRequest implements Validatable, ToXContentObject {
static final String KEY_TIEBREAKER_FIELD = "tiebreaker_field";
static final String KEY_EVENT_CATEGORY_FIELD = "event_category_field";
static final String KEY_IMPLICIT_JOIN_KEY_FIELD = "implicit_join_key_field";
static final String KEY_CASE_SENSITIVE = "case_sensitive";
static final String KEY_SIZE = "size";
static final String KEY_SEARCH_AFTER = "search_after";
static final String KEY_QUERY = "query";
@ -88,6 +91,8 @@ public class EqlSearchRequest implements Validatable, ToXContentObject {
builder.array(KEY_SEARCH_AFTER, searchAfterBuilder.getSortValues());
}
builder.field(KEY_CASE_SENSITIVE, isCaseSensitive());
builder.field(KEY_QUERY, query);
if (waitForCompletionTimeout != null) {
builder.field(KEY_WAIT_FOR_COMPLETION_TIMEOUT, waitForCompletionTimeout);
@ -152,6 +157,15 @@ public class EqlSearchRequest implements Validatable, ToXContentObject {
return this.implicitJoinKeyField;
}
public boolean isCaseSensitive() {
return this.isCaseSensitive;
}
public EqlSearchRequest isCaseSensitive(boolean isCaseSensitive) {
this.isCaseSensitive = isCaseSensitive;
return this;
}
public EqlSearchRequest implicitJoinKeyField(String implicitJoinKeyField) {
Objects.requireNonNull(implicitJoinKeyField, "implicit join key must not be null");
this.implicitJoinKeyField = implicitJoinKeyField;
@ -242,6 +256,7 @@ public class EqlSearchRequest implements Validatable, ToXContentObject {
Objects.equals(implicitJoinKeyField, that.implicitJoinKeyField) &&
Objects.equals(searchAfterBuilder, that.searchAfterBuilder) &&
Objects.equals(query, that.query) &&
Objects.equals(isCaseSensitive, that.isCaseSensitive) &&
Objects.equals(waitForCompletionTimeout, that.waitForCompletionTimeout) &&
Objects.equals(keepAlive, that.keepAlive) &&
Objects.equals(keepOnCompletion, that.keepOnCompletion);
@ -255,11 +270,12 @@ public class EqlSearchRequest implements Validatable, ToXContentObject {
filter,
fetchSize,
timestampField,
tiebreakerField,
tiebreakerField,
eventCategoryField,
implicitJoinKeyField,
searchAfterBuilder,
query,
isCaseSensitive,
waitForCompletionTimeout,
keepAlive,
keepOnCompletion);

View File

@ -9,111 +9,56 @@ package org.elasticsearch.test.eql;
import com.carrotsearch.randomizedtesting.annotations.ParametersFactory;
import org.elasticsearch.Build;
import org.elasticsearch.action.bulk.BulkRequest;
import org.elasticsearch.action.bulk.BulkResponse;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.support.WriteRequest;
import org.elasticsearch.client.EqlClient;
import org.elasticsearch.client.Request;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.ResponseException;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.client.eql.EqlSearchRequest;
import org.elasticsearch.client.eql.EqlSearchResponse;
import org.elasticsearch.client.eql.EqlSearchResponse.Hits;
import org.elasticsearch.client.eql.EqlSearchResponse.Sequence;
import org.elasticsearch.client.indices.CreateIndexRequest;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.io.Streams;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.common.xcontent.json.JsonXContent;
import org.elasticsearch.rest.RestStatus;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.test.rest.ESRestTestCase;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import static java.util.stream.Collectors.toList;
import static org.hamcrest.Matchers.instanceOf;
import static org.elasticsearch.test.eql.DataLoader.testIndexName;
public abstract class CommonEqlActionTestCase extends ESRestTestCase {
private RestHighLevelClient highLevelClient;
static final String indexPrefix = "endgame";
static final String testIndexName = indexPrefix + "-1.4.0";
protected static final String PARAM_FORMATTING = "%1$s.test -> %2$s";
private RestHighLevelClient highLevelClient;
@BeforeClass
public static void checkForSnapshot() {
assumeTrue("Only works on snapshot builds for now", Build.CURRENT.isSnapshot());
}
private static boolean isSetUp = false;
private static int counter = 0;
@SuppressWarnings("unchecked")
private static void setupData(CommonEqlActionTestCase tc) throws Exception {
if (isSetUp) {
return;
}
CreateIndexRequest request = new CreateIndexRequest(testIndexName)
.mapping(Streams.readFully(CommonEqlActionTestCase.class.getResourceAsStream("/mapping-default.json")),
XContentType.JSON);
tc.highLevelClient().indices().create(request, RequestOptions.DEFAULT);
BulkRequest bulk = new BulkRequest();
bulk.setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE);
try (XContentParser parser = tc.createParser(JsonXContent.jsonXContent,
CommonEqlActionTestCase.class.getResourceAsStream("/test_data.json"))) {
List<Object> list = parser.list();
for (Object item : list) {
assertThat(item, instanceOf(HashMap.class));
Map<String, Object> entry = (Map<String, Object>) item;
bulk.add(new IndexRequest(testIndexName).source(entry, XContentType.JSON));
}
}
if (bulk.numberOfActions() > 0) {
BulkResponse bulkResponse = tc.highLevelClient().bulk(bulk, RequestOptions.DEFAULT);
assertEquals(RestStatus.OK, bulkResponse.status());
assertFalse(bulkResponse.hasFailures());
isSetUp = true;
}
}
private static void cleanupData(CommonEqlActionTestCase tc) throws Exception {
// Delete index after all tests ran
if (isSetUp && (--counter == 0)) {
deleteIndex(testIndexName);
isSetUp = false;
}
}
@Override
protected boolean preserveClusterUponCompletion() {
// Need to preserve data between parameterized tests runs
return true;
}
@Before
public void setup() throws Exception {
setupData(this);
if (client().performRequest(new Request("HEAD", "/" + testIndexName)).getStatusLine().getStatusCode() == 404) {
DataLoader.loadDatasetIntoEs(highLevelClient(), (t, u) -> createParser(t, u));
}
}
@After
public void cleanup() throws Exception {
cleanupData(this);
@AfterClass
public static void cleanup() throws Exception {
try {
adminClient().performRequest(new Request("DELETE", "/*"));
} catch (ResponseException e) {
// 404 here just means we had no indexes
if (e.getResponse().getStatusLine().getStatusCode() != 404) {
throw e;
}
}
}
@ParametersFactory(shuffle = false, argumentFormatting = PARAM_FORMATTING)
@ -121,7 +66,7 @@ public abstract class CommonEqlActionTestCase extends ESRestTestCase {
// Load EQL validation specs
List<EqlSpec> specs = EqlSpecLoader.load("/test_queries.toml", true);
specs.addAll(EqlSpecLoader.load("/test_queries_supported.toml", true));
specs.addAll(EqlSpecLoader.load("/additional_test_queries.toml", true));
List<EqlSpec> unsupportedSpecs = EqlSpecLoader.load("/test_queries_unsupported.toml", false);
// Validate only currently supported specs
@ -131,7 +76,7 @@ public abstract class CommonEqlActionTestCase extends ESRestTestCase {
boolean supported = true;
// Check if spec is supported, simple iteration, cause the list is short.
for (EqlSpec unSpec : unsupportedSpecs) {
if (spec.query() != null && spec.query().equals(unSpec.query())) {
if (spec.equals(unSpec)) {
supported = false;
break;
}
@ -141,7 +86,6 @@ public abstract class CommonEqlActionTestCase extends ESRestTestCase {
filteredSpecs.add(spec);
}
}
counter = specs.size();
return asArray(filteredSpecs);
}
@ -171,7 +115,19 @@ public abstract class CommonEqlActionTestCase extends ESRestTestCase {
}
public void test() throws Exception {
assertResponse(runQuery(testIndexName, spec.query()));
// run both tests if case sensitivity doesn't matter
if (spec.caseSensitive() == null) {
assertResponse(runQuery(testIndexName, spec.query(), true));
assertResponse(runQuery(testIndexName, spec.query(), false));
}
// run only the case sensitive test
else if (spec.caseSensitive()) {
assertResponse(runQuery(testIndexName, spec.query(), true));
}
// run only the case insensitive test
else {
assertResponse(runQuery(testIndexName, spec.query(), false));
}
}
protected void assertResponse(EqlSearchResponse response) {
@ -187,14 +143,16 @@ public abstract class CommonEqlActionTestCase extends ESRestTestCase {
}
}
protected EqlSearchResponse runQuery(String index, String query) throws Exception {
protected EqlSearchResponse runQuery(String index, String query, boolean isCaseSensitive) throws Exception {
EqlSearchRequest request = new EqlSearchRequest(testIndexName, query);
request.isCaseSensitive(isCaseSensitive);
request.tiebreakerField("event.sequence");
return eqlClient().search(request, RequestOptions.DEFAULT);
return highLevelClient().eql().search(request, RequestOptions.DEFAULT);
}
protected EqlClient eqlClient() {
return highLevelClient().eql();
protected void assertSearchHits(List<SearchHit> events) {
assertNotNull(events);
assertArrayEquals("unexpected result for spec: [" + spec.toString() + "]", spec.expectedEventIds(), extractIds(events));
}
private static long[] extractIds(List<SearchHit> events) {
@ -206,11 +164,6 @@ public abstract class CommonEqlActionTestCase extends ESRestTestCase {
return ids;
}
protected void assertSearchHits(List<SearchHit> events) {
assertNotNull(events);
assertArrayEquals("unexpected result for spec: [" + spec.toString() + "]", spec.expectedEventIds(), extractIds(events));
}
protected void assertSequences(List<Sequence> sequences) {
List<SearchHit> events = sequences.stream()
.flatMap(s -> s.events().stream())
@ -229,4 +182,10 @@ public abstract class CommonEqlActionTestCase extends ESRestTestCase {
}
return highLevelClient;
}
@Override
protected boolean preserveClusterUponCompletion() {
// Need to preserve data between parameterized tests runs
return true;
}
}

View File

@ -0,0 +1,85 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.test.eql;
import org.apache.http.HttpHost;
import org.apache.logging.log4j.LogManager;
import org.elasticsearch.action.bulk.BulkRequest;
import org.elasticsearch.action.bulk.BulkResponse;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.support.WriteRequest;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.client.indices.CreateIndexRequest;
import org.elasticsearch.cluster.ClusterModule;
import org.elasticsearch.common.CheckedBiFunction;
import org.elasticsearch.common.io.Streams;
import org.elasticsearch.common.xcontent.LoggingDeprecationHandler;
import org.elasticsearch.common.xcontent.NamedXContentRegistry;
import org.elasticsearch.common.xcontent.XContent;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.common.xcontent.json.JsonXContent;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.List;
import java.util.Map;
public class DataLoader {
private static final String TEST_DATA = "/test_data.json";
private static final String MAPPING = "/mapping-default.json";
static final String indexPrefix = "endgame";
static final String testIndexName = indexPrefix + "-1.4.0";
public static void main(String[] args) throws IOException {
try (RestClient client = RestClient.builder(new HttpHost("localhost", 9200)).build()) {
loadDatasetIntoEs(new RestHighLevelClient(
client,
ignore -> {
},
Collections.emptyList()) {
}, (t, u) -> createParser(t, u));
}
}
@SuppressWarnings("unchecked")
protected static void loadDatasetIntoEs(RestHighLevelClient client,
CheckedBiFunction<XContent, InputStream, XContentParser, IOException> p) throws IOException {
CreateIndexRequest request = new CreateIndexRequest(testIndexName)
.mapping(Streams.readFully(DataLoader.class.getResourceAsStream(MAPPING)), XContentType.JSON);
client.indices().create(request, RequestOptions.DEFAULT);
BulkRequest bulk = new BulkRequest();
bulk.setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE);
try (XContentParser parser = p.apply(JsonXContent.jsonXContent, DataLoader.class.getResourceAsStream(TEST_DATA))) {
List<Object> list = parser.list();
for (Object item : list) {
bulk.add(new IndexRequest(testIndexName).source((Map<String, Object>) item, XContentType.JSON));
}
}
if (bulk.numberOfActions() > 0) {
BulkResponse bulkResponse = client.bulk(bulk, RequestOptions.DEFAULT);
if (bulkResponse.hasFailures()) {
LogManager.getLogger(DataLoader.class).info("Data FAILED loading");
} else {
LogManager.getLogger(DataLoader.class).info("Data loaded");
}
}
}
private static XContentParser createParser(XContent xContent, InputStream data) throws IOException {
NamedXContentRegistry contentRegistry = new NamedXContentRegistry(ClusterModule.getNamedXWriteables());
return xContent.createParser(contentRegistry, LoggingDeprecationHandler.INSTANCE, data);
}
}

View File

@ -9,6 +9,7 @@ package org.elasticsearch.test.eql;
import org.elasticsearch.common.Strings;
import java.util.Arrays;
import java.util.Objects;
public class EqlSpec {
private String description;
@ -17,6 +18,12 @@ public class EqlSpec {
private String query;
private long[] expectedEventIds;
// flag to dictate which modes are supported for the test
// null -> apply the test to both modes (case sensitive and case insensitive)
// TRUE -> case sensitive
// FALSE -> case insensitive
private Boolean caseSensitive = null;
public String description() {
return description;
}
@ -57,6 +64,14 @@ public class EqlSpec {
this.expectedEventIds = expectedEventIds;
}
public void caseSensitive(Boolean caseSensitive) {
this.caseSensitive = caseSensitive;
}
public Boolean caseSensitive() {
return this.caseSensitive;
}
@Override
public String toString() {
String str = "";
@ -64,6 +79,10 @@ public class EqlSpec {
str = appendWithComma(str, "description", description);
str = appendWithComma(str, "note", note);
if (caseSensitive != null) {
str = appendWithComma(str, "case_sensitive", Boolean.toString(caseSensitive));
}
if (tags != null) {
str = appendWithComma(str, "tags", Arrays.toString(tags));
}
@ -74,6 +93,27 @@ public class EqlSpec {
return str;
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (other == null || getClass() != other.getClass()) {
return false;
}
EqlSpec that = (EqlSpec) other;
return Objects.equals(this.query(), that.query())
&& Objects.equals(this.caseSensitive, that.caseSensitive);
}
@Override
public int hashCode() {
return Objects.hash(this.query, this.caseSensitive);
}
private static String appendWithComma(String str, String name, String append) {
if (!Strings.isNullOrEmpty(append)) {
if (!Strings.isNullOrEmpty(str)) {

View File

@ -55,6 +55,20 @@ public class EqlSpecLoader {
spec.note(getTrimmedString(table, "note"));
spec.description(getTrimmedString(table, "description"));
Boolean caseSensitive = table.getBoolean("case_sensitive");
Boolean caseInsensitive = table.getBoolean("case_insensitive");
// if case_sensitive is TRUE and case_insensitive is not TRUE (FALSE or NULL), then the test is case sensitive only
if (Boolean.TRUE.equals(caseSensitive)) {
if (Boolean.FALSE.equals(caseInsensitive) || caseInsensitive == null) {
spec.caseSensitive(true);
}
}
// if case_sensitive is not TRUE (FALSE or NULL) and case_insensitive is TRUE, then the test is case insensitive only
else if (Boolean.TRUE.equals(caseInsensitive)) {
spec.caseSensitive(false);
}
// in all other cases, the test should run no matter the case sensitivity (should test both scenarios)
List<?> arr = table.getList("tags");
if (arr != null) {
String tags[] = new String[arr.size()];

View File

@ -1,5 +1,5 @@
# This file is populated with additional EQL queries that were not present in the original EQL python implementation
# test_queries.toml file in order to keep the original unchanges and easier to sync with the EQL reference implementation tests.
# test_queries.toml file in order to keep the original unchanged and easier to sync with the EQL reference implementation tests.
[[queries]]
expected_event_ids = [95]
@ -186,6 +186,28 @@ query = "file where 66.0 / serial_event_id == 1"
expected_event_ids = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 46]
query = "process where serial_event_id + ((1 + 3) * 2 / (3 - 1)) * 2 == 54 or 70 + serial_event_id < 100"
[[queries]]
query = '''
sequence
[process where serial_event_id = 1]
[process where serial_event_id = 2]
'''
expected_event_ids = [1, 2]
[[queries]]
query = '''
sequence
[process where serial_event_id=1] by unique_pid
[process where true] by unique_ppid'''
expected_event_ids = [1, 2]
[[queries]]
query = '''
sequence
[process where serial_event_id<3] by unique_pid
[process where true] by unique_ppid
'''
expected_event_ids = [1, 2, 2, 3]
[[queries]]
query = '''
@ -193,4 +215,4 @@ sequence
[process where false] by unique_pid
[process where true] by unique_ppid
'''
expected_event_ids = []
expected_event_ids = []