Repository: oasis-open/cti-pattern-validator

Issue:
STIX 2.1 pattern validation failing on multiple START STOP qualifiers
When running the following from a terminal:
```
validate-patterns -v 2.1
[network-traffic:src_port = 37020 AND user-account:user_id = 'root'] START t'2016-06-01T01:30:00.123Z' STOP t'2016-06-01T02:20:00.123Z' OR [ipv4-addr:value = '192.168.122.83'] START t'2016-06-01T03:55:00.123Z' STOP t'2016-06-01T04:30:24.743Z'
```
I get a failure:
`FAIL: The same qualifier is used more than once`

I thought STIX patterns supported multiple START STOP qualifiers since the qualifier affects only the observation that immediately precedes it.  Am I wrong in this?

Comments:

Patch:
diff --git a/setup.py b/setup.py
index 36453d8..2ab745d 100644
--- a/setup.py
+++ b/setup.py
@@ -42,6 +42,7 @@ setup(
         'antlr4-python3-runtime~=4.8.0 ; python_version>="3"',
         'six',
         'typing ; python_version<"3.5" and python_version>="3"',
+        'enum34 ; python_version<"3.4"',
     ],
     package_data={
         'stix2patterns.test.v20': ['spec_examples.txt'],
diff --git a/stix2patterns/v21/validator.py b/stix2patterns/v21/validator.py
index 12a1a38..11adaed 100644
--- a/stix2patterns/v21/validator.py
+++ b/stix2patterns/v21/validator.py
@@ -2,14 +2,69 @@
 Validates a user entered pattern against STIXPattern grammar.
 """
 
+import enum
+
 from antlr4 import CommonTokenStream, ParseTreeWalker
 
 from . import object_validator
 from ..exceptions import STIXPatternErrorListener
 from .grammars.STIXPatternLexer import STIXPatternLexer
+from .grammars.STIXPatternListener import STIXPatternListener
 from .grammars.STIXPatternParser import STIXPatternParser
 from .inspector import InspectionListener
 
+QualType = enum.Enum("QualType", "WITHIN REPEATS STARTSTOP")
+
+
+class DuplicateQualifierTypeError(Exception):
+    """
+    Instances represent finding multiple qualifiers of the same type directly
+    applied to an observation expression (i.e. not on some parenthesized group
+    of which the observation expression is a member).
+    """
+    def __init__(self, qual_type):
+        """
+        Initialize this exception instance.
+
+        :param qual_type: The qualifier type which was found to be duplicated.
+            Must be a member of the QualType enum.
+        """
+        message = "Duplicate qualifier type encountered: " + qual_type.name
+
+        super(DuplicateQualifierTypeError, self).__init__(message)
+
+        self.qual_type = qual_type
+
+
+class ValidationListener(STIXPatternListener):
+    """
+    Does some pattern validation via a parse tree traversal.
+    """
+    def __init__(self):
+        self.__qual_types = None
+
+    def __check_qualifier_type(self, qual_type):
+        if self.__qual_types is not None:
+            if qual_type in self.__qual_types:
+                raise DuplicateQualifierTypeError(qual_type)
+            else:
+                self.__qual_types.add(qual_type)
+
+    def exitObservationExpressionSimple(self, ctx):
+        self.__qual_types = set()
+
+    def exitObservationExpressionCompound(self, ctx):
+        self.__qual_types = None
+
+    def exitObservationExpressionWithin(self, ctx):
+        self.__check_qualifier_type(QualType.WITHIN)
+
+    def exitObservationExpressionRepeated(self, ctx):
+        self.__check_qualifier_type(QualType.REPEATS)
+
+    def exitObservationExpressionStartStop(self, ctx):
+        self.__check_qualifier_type(QualType.STARTSTOP)
+
 
 def run_validator(pattern, start):
     """
@@ -38,7 +93,6 @@ def run_validator(pattern, start):
             parser.literalNames[i] = parser.symbolicNames[i]
 
     tree = parser.pattern()
-    inspection_listener = InspectionListener()
 
     # replace with easier-to-understand error message
     if not (start[0] == '[' or start == '(['):
@@ -47,6 +101,7 @@ def run_validator(pattern, start):
 
     # validate observed objects
     if len(parseErrListener.err_strings) == 0:
+        inspection_listener = InspectionListener()
         ParseTreeWalker.DEFAULT.walk(inspection_listener, tree)
         patt_data = inspection_listener.pattern_data()
 
@@ -56,9 +111,9 @@ def run_validator(pattern, start):
             parseErrListener.err_strings.extend(obj_validator_results)
 
         # check qualifiers
-        qualifiers = [q.split()[0] for q in patt_data.qualifiers]
-        if len(qualifiers) != len(set(qualifiers)):
-            parseErrListener.err_strings.insert(0, "FAIL: The same qualifier is"
-                                                   " used more than once")
+        try:
+            ParseTreeWalker.DEFAULT.walk(ValidationListener(), tree)
+        except DuplicateQualifierTypeError as e:
+            parseErrListener.err_strings.insert(0, "FAIL: " + e.args[0])
 
     return parseErrListener.err_strings


Test patch:
diff --git a/stix2patterns/test/v21/test_validator.py b/stix2patterns/test/v21/test_validator.py
index d78fc75..26f9076 100644
--- a/stix2patterns/test/v21/test_validator.py
+++ b/stix2patterns/test/v21/test_validator.py
@@ -51,7 +51,13 @@ FAIL_CASES = [
     ("[file:hashes.'SHA-256' = 'f00']",  # Malformed hash value
         "FAIL: 'f00' is not a valid SHA-256 hash"),
     ("[win-registry-key:key = 'hkey_local_machine\\\\foo\\\\bar'] WITHIN 5 SECONDS WITHIN 6 SECONDS",
-        "FAIL: The same qualifier is used more than once"),
+        "FAIL: Duplicate qualifier type encountered: WITHIN"),
+    ("([win-registry-key:key = 'hkey_local_machine\\\\foo\\\\bar'] REPEATS 2 TIMES REPEATS 3 TIMES)",
+        "FAIL: Duplicate qualifier type encountered: REPEATS"),
+    ("[win-registry-key:key = 'hkey_local_machine\\\\foo\\\\bar'] "
+     "START t'2016-06-01T01:30:00.123Z' STOP t'2016-06-01T02:20:00.123Z' "
+     "START t'2016-06-01T01:30:00.123Z' STOP t'2016-06-01T02:20:00.123Z'",
+        "FAIL: Duplicate qualifier type encountered: STARTSTOP"),
     # TODO: add more failing test cases.
 ]
 
@@ -89,6 +95,12 @@ PASS_CASES = [
     "[foo:bar=1] REPEATS 9 TIMES",
     "[network-traffic:start = '2018-04-20T12:36:24.558Z']",
     "( [(network-traffic:dst_port IN(443,6443,8443) AND network-traffic:src_packets != 0) ])",  # Misplaced whitespace
+    "([win-registry-key:key = 'hkey_local_machine\\\\foo\\\\bar']) WITHIN 5 SECONDS WITHIN 6 SECONDS",
+    "([win-registry-key:key = 'hkey_local_machine\\\\foo\\\\bar'] REPEATS 2 TIMES) REPEATS 2 TIMES",
+    "[network-traffic:src_port = 37020 AND user-account:user_id = 'root'] "
+    "START t'2016-06-01T01:30:00.123Z' STOP t'2016-06-01T02:20:00.123Z' OR "
+    "[ipv4-addr:value = '192.168.122.83'] "
+    "START t'2016-06-01T03:55:00.123Z' STOP t'2016-06-01T04:30:24.743Z'"
 ]