diff --git a/x-pack/plugin/eql/qa/common/src/main/resources/test_queries_supported.toml b/x-pack/plugin/eql/qa/common/src/main/resources/test_queries_supported.toml index 40574a85b64..546f7dede33 100644 --- a/x-pack/plugin/eql/qa/common/src/main/resources/test_queries_supported.toml +++ b/x-pack/plugin/eql/qa/common/src/main/resources/test_queries_supported.toml @@ -51,6 +51,32 @@ expected_event_ids = [1, 2, 3, 4] query = 'process where string(serial_event_id) = "1"' expected_event_ids = [1] + +[[queries]] +query = 'any where number(string(serial_event_id)) == 17' +expected_event_ids = [17] + + +[[queries]] +query = 'any where number(string(serial_event_id), null) == 17' +expected_event_ids = [17] + + +[[queries]] +query = 'any where number(string(serial_event_id), 10) == 17' +expected_event_ids = [17] + + +[[queries]] +query = 'any where number(string(serial_event_id), 13) == number("31", 13)' +expected_event_ids = [31] + + +[[queries]] +query = 'any where number(string(serial_event_id), 16) == 17' +expected_event_ids = [11] + + [[queries]] expected_event_ids = [98] notes = "regexp doesn't support character classes" diff --git a/x-pack/plugin/eql/qa/common/src/main/resources/test_queries_unsupported.toml b/x-pack/plugin/eql/qa/common/src/main/resources/test_queries_unsupported.toml index d34e2f0e128..71e44d57973 100644 --- a/x-pack/plugin/eql/qa/common/src/main/resources/test_queries_unsupported.toml +++ b/x-pack/plugin/eql/qa/common/src/main/resources/test_queries_unsupported.toml @@ -822,24 +822,8 @@ process where modulo(11, add(serial_event_id, 1)) == serial_event_id''' expected_event_ids = [1, 2, 3, 5, 11] description = "test built-in math functions" -[[queries]] -query = ''' -process where serial_event_id == number('5')''' -expected_event_ids = [5] -description = "test string/number conversions" - -[[queries]] -expected_event_ids = [50] -description = "test string/number conversions" -query = ''' -process where serial_event_id == number('0x32', 16)''' - -[[queries]] -expected_event_ids = [50] -description = "test string/number conversions" -query = ''' -process where serial_event_id == number('32', 16)''' +# this should be removed and the number function should be updated to take only strings [[queries]] query = ''' process where number(serial_event_id) == number(5)''' diff --git a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/expression/function/EqlFunctionRegistry.java b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/expression/function/EqlFunctionRegistry.java index 65da70a736d..e251a15742b 100644 --- a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/expression/function/EqlFunctionRegistry.java +++ b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/expression/function/EqlFunctionRegistry.java @@ -16,6 +16,7 @@ import org.elasticsearch.xpack.eql.expression.function.scalar.string.Match; import org.elasticsearch.xpack.eql.expression.function.scalar.string.StartsWith; import org.elasticsearch.xpack.eql.expression.function.scalar.string.StringContains; import org.elasticsearch.xpack.eql.expression.function.scalar.string.Substring; +import org.elasticsearch.xpack.eql.expression.function.scalar.math.ToNumber; import org.elasticsearch.xpack.eql.expression.function.scalar.string.ToString; import org.elasticsearch.xpack.eql.expression.function.scalar.string.Wildcard; import org.elasticsearch.xpack.ql.expression.function.FunctionDefinition; @@ -50,7 +51,7 @@ public class EqlFunctionRegistry extends FunctionRegistry { def(ToString.class, ToString::new, "string"), def(StringContains.class, StringContains::new, "stringcontains"), def(Substring.class, Substring::new, "substring"), - def(Wildcard.class, Wildcard::new, "wildcard") + def(Wildcard.class, Wildcard::new, "wildcard"), }, // Arithmetic new FunctionDefinition[] { @@ -58,7 +59,8 @@ public class EqlFunctionRegistry extends FunctionRegistry { def(Div.class, Div::new, "divide"), def(Mod.class, Mod::new, "modulo"), def(Mul.class, Mul::new, "multiply"), - def(Sub.class, Sub::new, "subtract") + def(ToNumber.class, ToNumber::new, "number"), + def(Sub.class, Sub::new, "subtract"), } }; } diff --git a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/expression/function/scalar/math/ToNumber.java b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/expression/function/scalar/math/ToNumber.java new file mode 100644 index 00000000000..f542633a069 --- /dev/null +++ b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/expression/function/scalar/math/ToNumber.java @@ -0,0 +1,116 @@ +/* + * 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.xpack.eql.expression.function.scalar.math; + +import org.elasticsearch.xpack.ql.expression.Expression; +import org.elasticsearch.xpack.ql.expression.Expressions; +import org.elasticsearch.xpack.ql.expression.Expressions.ParamOrdinal; +import org.elasticsearch.xpack.ql.expression.FieldAttribute; +import org.elasticsearch.xpack.ql.expression.Literal; +import org.elasticsearch.xpack.ql.expression.function.OptionalArgument; +import org.elasticsearch.xpack.ql.expression.function.scalar.ScalarFunction; +import org.elasticsearch.xpack.ql.expression.gen.pipeline.Pipe; +import org.elasticsearch.xpack.ql.expression.gen.script.ScriptTemplate; +import org.elasticsearch.xpack.ql.expression.gen.script.Scripts; +import org.elasticsearch.xpack.ql.tree.NodeInfo; +import org.elasticsearch.xpack.ql.tree.Source; +import org.elasticsearch.xpack.ql.type.DataType; +import org.elasticsearch.xpack.ql.type.DataTypes; + +import java.util.Arrays; +import java.util.List; +import java.util.Locale; + +import static java.lang.String.format; +import static org.elasticsearch.xpack.eql.expression.function.scalar.math.ToNumberFunctionProcessor.doProcess; +import static org.elasticsearch.xpack.ql.expression.TypeResolutions.isInteger; +import static org.elasticsearch.xpack.ql.expression.TypeResolutions.isStringAndExact; +import static org.elasticsearch.xpack.ql.expression.gen.script.ParamsBuilder.paramsBuilder; + +/** + * EQL specific function for parsing strings into numbers. + */ +public class ToNumber extends ScalarFunction implements OptionalArgument { + + private final Expression value, base; + + public ToNumber(Source source, Expression value, Expression base) { + super(source, Arrays.asList(value, base != null ? base : new Literal(source, null, DataTypes.NULL))); + this.value = value; + this.base = arguments().get(1); + } + + @Override + protected TypeResolution resolveType() { + if (!childrenResolved()) { + return new TypeResolution("Unresolved children"); + } + + TypeResolution valueResolution = isStringAndExact(value, sourceText(), ParamOrdinal.FIRST); + if (valueResolution.unresolved()) { + return valueResolution; + } + + return isInteger(base, sourceText(), ParamOrdinal.SECOND); + } + + @Override + protected Pipe makePipe() { + return new ToNumberFunctionPipe(source(), this, Expressions.pipe(value), Expressions.pipe(base)); + } + + @Override + public boolean foldable() { + return value.foldable() && base.foldable(); + } + + @Override + public Object fold() { + return doProcess(value.fold(), base.fold()); + } + + @Override + protected NodeInfo info() { + return NodeInfo.create(this, ToNumber::new, value, base); + } + + @Override + public ScriptTemplate asScript() { + ScriptTemplate valueScript = asScript(value); + ScriptTemplate baseScript = asScript(base); + + return new ScriptTemplate(format(Locale.ROOT, formatTemplate("{eql}.%s(%s,%s)"), + "number", + valueScript.template(), + baseScript.template()), + paramsBuilder() + .script(valueScript.params()) + .script(baseScript.params()) + .build(), dataType()); + } + + @Override + public ScriptTemplate scriptWithField(FieldAttribute field) { + return new ScriptTemplate(processScript(Scripts.DOC_VALUE), + paramsBuilder().variable(field.exactAttribute().name()).build(), + dataType()); + } + + @Override + public DataType dataType() { + return DataTypes.DOUBLE; + } + + @Override + public Expression replaceChildren(List newChildren) { + if (newChildren.size() != 2) { + throw new IllegalArgumentException("expected [2] children but received [" + newChildren.size() + "]"); + } + + return new ToNumber(source(), newChildren.get(0), newChildren.get(1)); + } +} diff --git a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/expression/function/scalar/math/ToNumberFunctionPipe.java b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/expression/function/scalar/math/ToNumberFunctionPipe.java new file mode 100644 index 00000000000..d0e37ea2fa4 --- /dev/null +++ b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/expression/function/scalar/math/ToNumberFunctionPipe.java @@ -0,0 +1,44 @@ +/* + * 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.xpack.eql.expression.function.scalar.math; + +import org.elasticsearch.xpack.ql.expression.Expression; +import org.elasticsearch.xpack.ql.expression.gen.pipeline.Pipe; +import org.elasticsearch.xpack.ql.tree.NodeInfo; +import org.elasticsearch.xpack.ql.tree.Source; + +import java.util.Arrays; +import java.util.List; + +public class ToNumberFunctionPipe extends Pipe { + + private final Pipe value, base; + + public ToNumberFunctionPipe(Source source, Expression expression, Pipe value, Pipe base) { + super(source, expression, Arrays.asList(value, base)); + this.value = value; + this.base = base; + + } + + @Override + public final Pipe replaceChildren(List newChildren) { + if (newChildren.size() != 2) { + throw new IllegalArgumentException("expected [2] children but received [" + newChildren.size() + "]"); + } + return new ToNumberFunctionPipe(source(), expression(), newChildren.get(0), newChildren.get(1)); + } + + @Override + protected NodeInfo info() { + return NodeInfo.create(this, ToNumberFunctionPipe::new, expression(), value, base); + } + + @Override + public ToNumberFunctionProcessor asProcessor() { + return new ToNumberFunctionProcessor(value.asProcessor(), base.asProcessor()); + } +} diff --git a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/expression/function/scalar/math/ToNumberFunctionProcessor.java b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/expression/function/scalar/math/ToNumberFunctionProcessor.java new file mode 100644 index 00000000000..b6756bf7912 --- /dev/null +++ b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/expression/function/scalar/math/ToNumberFunctionProcessor.java @@ -0,0 +1,118 @@ +/* + * 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.xpack.eql.expression.function.scalar.math; + +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.xpack.eql.EqlIllegalArgumentException; +import org.elasticsearch.xpack.ql.expression.gen.processor.Processor; + +import java.io.IOException; +import java.util.Objects; + +public class ToNumberFunctionProcessor implements Processor { + + public static final String NAME = "num"; + + private final Processor value, base; + + public ToNumberFunctionProcessor(Processor value, Processor base) { + this.value = value; + this.base = base; + } + + public ToNumberFunctionProcessor(StreamInput in) throws IOException { + value = in.readNamedWriteable(Processor.class); + base = in.readNamedWriteable(Processor.class); + } + + @Override + public final void writeTo(StreamOutput out) throws IOException { + out.writeNamedWriteable(value); + out.writeNamedWriteable(base); + } + + @Override + public Object process(Object input) { + return doProcess(value.process(input), base.process(input)); + } + + private static Number parseDecimal(String source) { + try { + return Long.valueOf(source); + } catch (NumberFormatException e) { + return Double.valueOf(source); + } + } + + public static Object doProcess(Object value, Object base) { + if (value == null) { + return null; + } + + if (!(value instanceof String || value instanceof Character)) { + throw new EqlIllegalArgumentException("A string/char is required; received [{}]", value); + } + + boolean detectedHexPrefix = value.toString().startsWith("0x"); + + if (base == null) { + base = detectedHexPrefix ? 16 : 10; + } else if (base instanceof Integer == false) { + throw new EqlIllegalArgumentException("An integer base is required; received [{}]", base); + } + + int radix = (Integer) base; + + if (detectedHexPrefix && radix == 16) { + value = value.toString().substring(2); + } + + try { + if (radix == 10) { + return parseDecimal(value.toString()); + } else { + return Long.parseLong(value.toString(), radix); + } + } catch (NumberFormatException e) { + throw new EqlIllegalArgumentException("Unable to convert [{}] to number of base [{}]", value, radix); + } + + } + + protected Processor value() { + return value; + } + + protected Processor base() { + return base; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + if (obj == null || getClass() != obj.getClass()) { + return false; + } + + ToNumberFunctionProcessor other = (ToNumberFunctionProcessor) obj; + return Objects.equals(value(), other.value()) && Objects.equals(base(), other.base()); + } + + @Override + public int hashCode() { + return Objects.hash(value, base); + } + + + @Override + public String getWriteableName() { + return NAME; + } +} diff --git a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/expression/function/scalar/whitelist/InternalEqlScriptUtils.java b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/expression/function/scalar/whitelist/InternalEqlScriptUtils.java index 4212fe06d9f..8873ba0f924 100644 --- a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/expression/function/scalar/whitelist/InternalEqlScriptUtils.java +++ b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/expression/function/scalar/whitelist/InternalEqlScriptUtils.java @@ -13,6 +13,7 @@ import org.elasticsearch.xpack.eql.expression.function.scalar.string.IndexOfFunc import org.elasticsearch.xpack.eql.expression.function.scalar.string.LengthFunctionProcessor; import org.elasticsearch.xpack.eql.expression.function.scalar.string.StringContainsFunctionProcessor; import org.elasticsearch.xpack.eql.expression.function.scalar.string.SubstringFunctionProcessor; +import org.elasticsearch.xpack.eql.expression.function.scalar.math.ToNumberFunctionProcessor; import org.elasticsearch.xpack.eql.expression.function.scalar.string.ToStringFunctionProcessor; import org.elasticsearch.xpack.ql.expression.function.scalar.whitelist.InternalQlScriptUtils; @@ -55,6 +56,10 @@ public class InternalEqlScriptUtils extends InternalQlScriptUtils { return (Boolean) StringContainsFunctionProcessor.doProcess(string, substring); } + public static Number number(String source, Number base) { + return (Number) ToNumberFunctionProcessor.doProcess(source, base); + } + public static String substring(String s, Number start, Number end) { return (String) SubstringFunctionProcessor.doProcess(s, start, end); } diff --git a/x-pack/plugin/eql/src/main/resources/org/elasticsearch/xpack/eql/plugin/eql_whitelist.txt b/x-pack/plugin/eql/src/main/resources/org/elasticsearch/xpack/eql/plugin/eql_whitelist.txt index 731f09d4660..c6fe606855b 100644 --- a/x-pack/plugin/eql/src/main/resources/org/elasticsearch/xpack/eql/plugin/eql_whitelist.txt +++ b/x-pack/plugin/eql/src/main/resources/org/elasticsearch/xpack/eql/plugin/eql_whitelist.txt @@ -70,6 +70,7 @@ class org.elasticsearch.xpack.eql.expression.function.scalar.whitelist.InternalE Boolean endsWith(String, String) Integer indexOf(String, String, Number) Integer length(String) + Number number(String, Number) String string(Object) Boolean stringContains(String, String) String substring(String, Number, Number) diff --git a/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/analysis/VerifierTests.java b/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/analysis/VerifierTests.java index 5608b09be27..1b7c5021e66 100644 --- a/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/analysis/VerifierTests.java +++ b/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/analysis/VerifierTests.java @@ -116,12 +116,6 @@ public class VerifierTests extends ESTestCase { error("network where safe(process_name)")); } - // Test the known EQL functions that are not supported - public void testFunctionVerificationUnknown() { - assertEquals("1:34: Unknown function [number]", - error("process where serial_event_id == number('5')")); - } - // Test unsupported array indexes public void testArrayIndexesUnsupported() { assertEquals("1:84: Array indexes are not supported", @@ -323,13 +317,13 @@ public class VerifierTests extends ESTestCase { assertEquals("1:11: Cannot use field [multi_field_nested.dep_name] type [text] with unsupported nested type in hierarchy " + "(field [multi_field_nested])", error(idxr, "foo where multi_field_nested.dep_name == 'bar'")); - assertEquals("1:11: Cannot use field [multi_field_nested.dep_id.keyword] type [keyword] with unsupported nested type in " + + assertEquals("1:11: Cannot use field [multi_field_nested.dep_id.keyword] type [keyword] with unsupported nested type in " + "hierarchy (field [multi_field_nested])", error(idxr, "foo where multi_field_nested.dep_id.keyword == 'bar'")); - assertEquals("1:11: Cannot use field [multi_field_nested.end_date] type [datetime] with unsupported nested type in " + + assertEquals("1:11: Cannot use field [multi_field_nested.end_date] type [datetime] with unsupported nested type in " + "hierarchy (field [multi_field_nested])", error(idxr, "foo where multi_field_nested.end_date == ''")); - assertEquals("1:11: Cannot use field [multi_field_nested.start_date] type [datetime] with unsupported nested type in " + + assertEquals("1:11: Cannot use field [multi_field_nested.start_date] type [datetime] with unsupported nested type in " + "hierarchy (field [multi_field_nested])", error(idxr, "foo where multi_field_nested.start_date == 'bar'")); } diff --git a/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/expression/function/scalar/math/ToNumberProcessorTests.java b/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/expression/function/scalar/math/ToNumberProcessorTests.java new file mode 100644 index 00000000000..5354924d559 --- /dev/null +++ b/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/expression/function/scalar/math/ToNumberProcessorTests.java @@ -0,0 +1,167 @@ +/* + * 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.xpack.eql.expression.function.scalar.math; + +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.ql.QlIllegalArgumentException; + +import static org.elasticsearch.xpack.ql.expression.function.scalar.FunctionTestUtils.l; +import static org.elasticsearch.xpack.ql.tree.Source.EMPTY; + + +public class ToNumberProcessorTests extends ESTestCase { + + private static Object process(Object value, Object base) { + return new ToNumber(EMPTY, l(value), l(base)).makePipe().asProcessor().process(null); + } + + private static String error(Object value, Object base) { + QlIllegalArgumentException saie = expectThrows(QlIllegalArgumentException.class, + () -> new ToNumber(EMPTY, l(value), l(base)).makePipe().asProcessor().process(null)); + return saie.getMessage(); + } + + public void toNumberWithLongRange() { + long number = randomLongBetween(Integer.MAX_VALUE, Long.MAX_VALUE); + + assertEquals(number, process(Long.toString(number), null)); + assertEquals(number, process("0x" + Long.toHexString(number), null)); + + assertEquals(number, process(Long.toString(number), 10)); + assertEquals(number, process(Long.toOctalString(number), 8)); + assertEquals(number, process(Long.toHexString(number), 16)); + assertEquals(number, process("0x" + Long.toHexString(number), 16)); + } + + public void toNumberWithPositiveInteger() { + int number = randomIntBetween(0, 1000); + + assertEquals(number, process(Integer.toString(number), null)); + assertEquals(number, process("0x" + Integer.toHexString(number), null)); + + assertEquals(number, process(Integer.toString(number), 10)); + assertEquals(number, process(Integer.toOctalString(number), 8)); + assertEquals(number, process(Integer.toHexString(number), 16)); + assertEquals(number, process("0x" + Integer.toHexString(number), 16)); + } + + public void toNumberWithNegativeInteger() { + int posInt = randomIntBetween(1, 1000); + int negInt = -posInt; + + assertEquals(negInt, process(Integer.toString(negInt), null)); + + assertEquals(negInt, process(Integer.toString(negInt), 10)); + assertEquals(negInt, process("-" + Integer.toOctalString(posInt), 8)); + assertEquals(negInt, process("-" + Integer.toHexString(posInt), 16)); + + assertEquals(negInt, process("-0x" + Integer.toHexString(posInt), 16)); + } + + public void toNumberWithPositiveFloat() { + double number = randomDoubleBetween(0.0, 1000.0, true); + + assertEquals(number, process(Double.toString(number), null)); + assertEquals(number, process(Double.toString(number), 10)); + } + + public void toNumberWithNegativeFloat() { + double number = randomDoubleBetween(-1000.0, -0.1, true); + + assertEquals(number, process(Double.toString(number), null)); + assertEquals(number, process(Double.toString(number), 10)); + } + + public void toNumberWithMissingInput() { + assertNull(process(null, null)); + assertNull(process(null, 8)); + assertNull(process(null, 10)); + assertNull(process(null, 16)); + } + + public void toNumberWithPositiveExponent() { + int number = randomIntBetween(-100, 100); + int exponent = randomIntBetween(0, 20); + + double expected = Math.pow((double) number, (double) exponent); + + assertEquals(expected, process(number + "e" + exponent, null)); + assertEquals(expected, process(number + "e" + exponent, 10)); + } + + public void toNumberWithNegativeExponent() { + int number = randomIntBetween(-100, 100); + int exponent = randomIntBetween(-10, -1); + + double expected = Math.pow(number, exponent); + + assertEquals(expected, process(number + "e-" + exponent, null)); + assertEquals(expected, process(number + "e-" + exponent, 10)); + } + + public void toNumberWithLocales() { + assertEquals("Unable to convert [1,000] to number of base [10]", + error("1,000", 7)); + assertEquals("Unable to convert [1,000] to number of base [10]", + error("1,000,000", 7)); + assertEquals("Unable to convert [1,000] to number of base [10]", + error("1.000.000", 7)); + assertEquals("Unable to convert [1,000] to number of base [10]", + error("1,000.000.000", 7)); + } + + public void toNumberWithUnsupportedDoubleBase() { + // test that only base 10 fractions are supported + double decimal = randomDouble(); + assertEquals("Unable to convert [1.0] to number of base [7]", + error(Double.toString(decimal), 7)); + assertEquals("Unable to convert [1.0] to number of base [8]", + error(Double.toString(decimal), 8)); + assertEquals("Unable to convert [1.0] to number of base [16]", + error(Double.toString(decimal), 16)); + } + + public void testNegativeBase16() { + assertEquals("Unable to convert [-0x1] to number of base [16]", + error("-0x1", 16)); + } + + public void testNumberInvalidDataType() { + assertEquals("A string/char is required; received [false]", + error(false, null)); + assertEquals("A string/char is required; received [1.0]", + error(1.0, null)); + assertEquals("A string/char is required; received [1]", + error(1, null)); + } + + public void testInvalidBase() { + int number = randomIntBetween(-100, 100); + + assertEquals("An integer base is required; received [foo]", + error(Integer.toString(number), "foo")); + assertEquals("An integer base is required; received [1.0]", + error(Integer.toString(number), 1.0)); + assertEquals("An integer base is required; received [false]", + error(Integer.toString(number), false)); + } + + public void testInvalidSourceString() { + assertEquals("Unable to convert [] to number of base [10]", + error("", null)); + assertEquals("Unable to convert [] to number of base [16]", + error("", 16)); + assertEquals("Unable to convert [foo] to number of base [10]", + error("foo", null)); + assertEquals("Unable to convert [foo] to number of base [16]", + error("foo", 16)); + assertEquals("Unable to convert [1.2.3.4] to number of base [10]", + error("1.2.3.4", 10)); + assertEquals("Unable to convert [1.2.3.4] to number of base [16]", + error("1.2.3.4", 16)); + } +} diff --git a/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/planner/QueryFolderFailTests.java b/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/planner/QueryFolderFailTests.java index 65afe031153..34592f2e38a 100644 --- a/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/planner/QueryFolderFailTests.java +++ b/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/planner/QueryFolderFailTests.java @@ -158,6 +158,32 @@ public class QueryFolderFailTests extends AbstractQueryFolderTestCase { "must be [string], found value [1] type [integer]", msg); } + public void testNumberFunctionAlreadyNumber() { + VerificationException e = expectThrows(VerificationException.class, + () -> plan("process where number(pid) == 1")); + String msg = e.getMessage(); + assertEquals("Found 1 problem\nline 1:15: first argument of [number(pid)] must be [string], " + + "found value [pid] type [long]", msg); + } + + public void testNumberFunctionFloatBase() { + VerificationException e = expectThrows(VerificationException.class, + () -> plan("process where number(process_name, 1.0) == 1")); + String msg = e.getMessage(); + assertEquals("Found 1 problem\nline 1:15: second argument of [number(process_name, 1.0)] must be [integer], " + + "found value [1.0] type [double]", msg); + + } + + public void testNumberFunctionNonString() { + VerificationException e = expectThrows(VerificationException.class, + () -> plan("process where number(plain_text) == 1")); + String msg = e.getMessage(); + assertEquals("Found 1 problem\nline 1:15: [number(plain_text)] cannot operate on first argument field of data type " + + "[text]: No keyword/multi-field defined exact matches for [plain_text]; define one or use MATCH/QUERY instead", msg); + + } + public void testPropertyEquationFilterUnsupported() { QlIllegalArgumentException e = expectThrows(QlIllegalArgumentException.class, () -> plan("process where (serial_event_id<9 and serial_event_id >= 7) or (opcode == pid)")); @@ -191,7 +217,6 @@ public class QueryFolderFailTests extends AbstractQueryFolderTestCase { + "[text]: No keyword/multi-field defined exact matches for [plain_text]; define one or use MATCH/QUERY instead", msg); } - public void testStringContainsWrongParams() { assertEquals("1:16: error building [stringcontains]: expects exactly two arguments", errorParsing("process where stringContains()")); diff --git a/x-pack/plugin/eql/src/test/resources/queryfolder_tests.txt b/x-pack/plugin/eql/src/test/resources/queryfolder_tests.txt index 0753063103f..f8748a85eff 100644 --- a/x-pack/plugin/eql/src/test/resources/queryfolder_tests.txt +++ b/x-pack/plugin/eql/src/test/resources/queryfolder_tests.txt @@ -257,6 +257,12 @@ process where match(command_line, "^.*?net.exe", "net\\.exe", "C:\\\\Windows\\\\ "regexp":{"command_line":{"value":"^.*?net.exe|net\\.exe|C:\\\\Windows\\\\system32\\\\net1\\s+" ; +numberFunctionSingleArgument +process where number(process_name) == 1; +InternalEqlScriptUtils.number(InternalQlScriptUtils.docValue(doc,params.v0),params.v1) +"params":{"v0":"process_name","v1":null,"v2":1} +; + matchFunctionScalar process where match(substring(command_line, 5), "^.*?net.exe", "net\\.exe", "C:\\\\Windows\\\\system32\\\\net1\\s+") ; @@ -265,6 +271,30 @@ InternalQlScriptUtils.docValue(doc,params.v0),params.v1,params.v2),params.v3))", "params":{"v0":"command_line","v1":5,"v2":null,"v3":"^.*?net.exe|net\\.exe|C:\\\\Windows\\\\system32\\\\net1\\s+"}} ; + +numberFunctionTwoFieldArguments +process where number(process_name, pid) != null; +InternalEqlScriptUtils.number(InternalQlScriptUtils.docValue(doc,params.v0),InternalQlScriptUtils.docValue(doc,params.v1))))", +"params":{"v0":"process_name","v1":"pid"} +; + +numberFunctionTwoArguments +process where number(process_name, 16) != null; +InternalEqlScriptUtils.number(InternalQlScriptUtils.docValue(doc,params.v0),params.v1) +"params":{"v0":"process_name","v1":16} +; + +numberFunctionFoldedComparison +process where serial_event_id == number("-32.5"); +{"term":{"serial_event_id":{"value":-32.5, +; + +numberFunctionFoldedHexComparison +process where serial_event_id == number("0x32", 16); +{"term":{"serial_event_id":{"value":50, +; + + wildcardFunctionSingleArgument process where wildcard(process_path, "*\\red_ttp\\wininit.*") ;