Repository: oasis-open/cti-pattern-validator

Issue:
Brackets check doesn't account for arbitrary nested parentheses
There is code to add a simpler error message in one particular case: when the user has forgotten square brackets around the pattern.  But it is *too* specific: it requires that patterns start with either `[` or `([`:

https://github.com/oasis-open/cti-pattern-validator/blob/2dafa44d3ea37faefaaa92e703f2ac481485de31/stix2patterns/v21/validator.py#L98

Patterns can have arbitrarily nested structure, so the pattern can start with an arbitrary number of open parentheses.  The check needs to be generalized.

Comments:

Patch:
diff --git a/stix2patterns/helpers.py b/stix2patterns/helpers.py
index 1cd284c..177890d 100644
--- a/stix2patterns/helpers.py
+++ b/stix2patterns/helpers.py
@@ -1,22 +1,29 @@
-import string
+import six
 
 
-def leading_characters(s, length):
+def brackets_check(pattern):
     """
-    Returns non-whitespace leading characters
+    Check whether the pattern is missing square brackets, in a way which does
+    not require the usual parsing.  This is a light hack to provide an improved
+    error message in this particular case.
 
-    :param str s: The string to process
-    :param int length: The number of characters to return
-    :return: The non-whitespace leading characters
-    :rtype: str or None
+    :param pattern: A STIX pattern string
+    :return: True if the pattern had its brackets; False if not
     """
-    if s is None:
-        return None
+    if isinstance(pattern, six.string_types):
 
-    stripped = []
-    for char in s:
-        if char not in string.whitespace:
-            stripped.append(char)
+        # There can be an arbitrary number of open parens first... skip over
+        # those
+        for c in pattern:
+            if c != "(" and not c.isspace():
+                break
 
-    upper_bound = min(length, len(stripped))
-    return ''.join(stripped[:upper_bound])
+        if c == "[":
+            result = True
+        else:
+            result = False
+
+    else:
+        result = False
+
+    return result
diff --git a/stix2patterns/v20/object_validator.py b/stix2patterns/v20/object_validator.py
index 72f051c..d92d34a 100644
--- a/stix2patterns/v20/object_validator.py
+++ b/stix2patterns/v20/object_validator.py
@@ -1,4 +1,5 @@
 import re
+
 import stix2patterns.inspector
 
 HASHES_REGEX = {
diff --git a/stix2patterns/v20/validator.py b/stix2patterns/v20/validator.py
index b1cf78a..8520cea 100644
--- a/stix2patterns/v20/validator.py
+++ b/stix2patterns/v20/validator.py
@@ -11,7 +11,7 @@ from .grammars.STIXPatternParser import STIXPatternParser
 from .inspector import InspectionListener
 
 
-def run_validator(pattern, start):
+def run_validator(pattern):
     """
     Validates a pattern against the STIX Pattern grammar.  Error messages are
     returned in a list.  The test passed if the returned list is empty.
@@ -40,11 +40,6 @@ def run_validator(pattern, start):
     tree = parser.pattern()
     inspection_listener = InspectionListener()
 
-    # replace with easier-to-understand error message
-    if not (start[0] == '[' or start == '(['):
-        parseErrListener.err_strings.insert(0, "FAIL: Error found at line 1:0. "
-                                               "input is missing square brackets")
-
     # validate observed objects
     if len(parseErrListener.err_strings) == 0:
         ParseTreeWalker.DEFAULT.walk(inspection_listener, tree)
diff --git a/stix2patterns/v21/object_validator.py b/stix2patterns/v21/object_validator.py
index 7558ba5..145d957 100644
--- a/stix2patterns/v21/object_validator.py
+++ b/stix2patterns/v21/object_validator.py
@@ -1,4 +1,5 @@
 import re
+
 import stix2patterns.inspector
 
 HASHES_REGEX = {
diff --git a/stix2patterns/v21/validator.py b/stix2patterns/v21/validator.py
index 11adaed..ac284f0 100644
--- a/stix2patterns/v21/validator.py
+++ b/stix2patterns/v21/validator.py
@@ -66,7 +66,7 @@ class ValidationListener(STIXPatternListener):
         self.__check_qualifier_type(QualType.STARTSTOP)
 
 
-def run_validator(pattern, start):
+def run_validator(pattern):
     """
     Validates a pattern against the STIX Pattern grammar.  Error messages are
     returned in a list.  The test passed if the returned list is empty.
@@ -94,11 +94,6 @@ def run_validator(pattern, start):
 
     tree = parser.pattern()
 
-    # replace with easier-to-understand error message
-    if not (start[0] == '[' or start == '(['):
-        parseErrListener.err_strings.insert(0, "FAIL: Error found at line 1:0. "
-                                               "input is missing square brackets")
-
     # validate observed objects
     if len(parseErrListener.err_strings) == 0:
         inspection_listener = InspectionListener()
diff --git a/stix2patterns/validator.py b/stix2patterns/validator.py
index 45e1634..48dea76 100644
--- a/stix2patterns/validator.py
+++ b/stix2patterns/validator.py
@@ -11,7 +11,7 @@ import six
 
 from . import DEFAULT_VERSION
 from .exceptions import STIXPatternErrorListener  # noqa: F401
-from .helpers import leading_characters
+from .helpers import brackets_check
 from .v20.validator import run_validator as run_validator20
 from .v21.validator import run_validator as run_validator21
 
@@ -21,19 +21,26 @@ def run_validator(pattern, stix_version=DEFAULT_VERSION):
     Validates a pattern against the STIX Pattern grammar.  Error messages are
     returned in a list.  The test passed if the returned list is empty.
     """
-    start = ''
     if isinstance(pattern, six.string_types):
-        start = leading_characters(pattern, 2)
+        pattern_str = pattern
         pattern = InputStream(pattern)
 
-    if not start:
-        start = leading_characters(pattern.readline(), 2)
+    else:
+        pattern_str = pattern.readline()
         pattern.seek(0)
 
     if stix_version == '2.1':
-        return run_validator21(pattern, start)
+        err_messages = run_validator21(pattern)
     else:
-        return run_validator20(pattern, start)
+        err_messages = run_validator20(pattern)
+
+    if not brackets_check(pattern_str):
+        err_messages.insert(
+            0,
+            "FAIL: Error found at line 1:0. input is missing square brackets"
+        )
+
+    return err_messages
 
 
 def validate(user_input, stix_version=DEFAULT_VERSION, ret_errs=False, print_errs=False):


Test patch:
diff --git a/stix2patterns/test/test_helpers.py b/stix2patterns/test/test_helpers.py
index 06c621e..7e15846 100644
--- a/stix2patterns/test/test_helpers.py
+++ b/stix2patterns/test/test_helpers.py
@@ -1,14 +1,33 @@
 """
 Test cases for stix2patterns/helpers.py.
 """
+import pytest
 
-from stix2patterns.helpers import leading_characters
+from stix2patterns.helpers import brackets_check
 
 
-def test_leading_characters():
+@pytest.mark.parametrize(
+    "value", [
+        '[file:size = 1280]',
+        ' [file:size = 1280]',
+        '( [file:size = 1280])',
+        '( ( [file:size = 1280]) )',
+        '(( ( ( [file:size = 1280])) ))',
+        '[',
+    ],
+)
+def test_brackets_check(value):
+    assert brackets_check(value)
 
-    assert leading_characters('[file:size = 1280]', 2) == '[f'
-    assert leading_characters(' [file:size = 1280]', 2) == '[f'
-    assert leading_characters('( [file:size = 1280])', 2) == '(['
-    assert leading_characters('[', 2) == '['
-    assert leading_characters(None, 2) is None
+
+@pytest.mark.parametrize(
+    "value", [
+        None,
+        "file:size = 1280",
+        "(file:size = 1280)",
+        " ( file:size = 1280 ) ",
+        " (( (( file:size = 1280 ) )) ) ",
+    ]
+)
+def test_brackets_check_fail(value):
+    assert not brackets_check(None)