diff --git a/.travis.yml b/.travis.yml
index 26be7f2..261f125 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,4 +1,4 @@
-os: linux
+sudo: false
 language: python
 cache: pip
 python:
@@ -6,15 +6,17 @@ python:
   - "3.4"
   - "3.5"
   - "3.6"
-  - "3.7"
-  - "3.8"
+matrix:
+  include:
+    - python: 3.7 # https://github.com/travis-ci/travis-ci/issues/9069#issuecomment-425720905
+      dist: xenial
+      sudo: true
 install:
   - pip install -U pip setuptools
-  - pip install tox-travis
-  - if [[ $TRAVIS_PYTHON_VERSION != 3.4 ]]; then pip install pre-commit; fi
+  - pip install tox-travis pre-commit
   - pip install codecov
 script:
   - tox
-  - if [[ $TRAVIS_PYTHON_VERSION != 3.4 ]]; then pre-commit run --all-files; fi
+  - pre-commit run --all-files
 after_success:
   - codecov
diff --git a/setup.py b/setup.py
index 36453d8..f55d406 100644
--- a/setup.py
+++ b/setup.py
@@ -20,7 +20,7 @@ dev_requires = doc_requires + test_requires + [
     'bumpversion',
     'check-manifest',
     'pre-commit',
-    # test_requires are installed into every tox environment, so we don't
+    # test_requires are installed into every tox environemnt, so we don't
     # want to include tox there.
     'tox',
 ]
@@ -38,10 +38,11 @@ setup(
     maintainer_email='clenk@mitre.org',
     packages=find_packages(),
     install_requires=[
-        'antlr4-python2-runtime~=4.8.0 ; python_version<"3"',
-        'antlr4-python3-runtime~=4.8.0 ; python_version>="3"',
+        'antlr4-python2-runtime>=4.7 ; python_version<"3"',
+        'antlr4-python3-runtime>=4.7 ; 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/v20/grammars/STIXPatternLexer.py b/stix2patterns/v20/grammars/STIXPatternLexer.py
index db09c9c..a6a1f51 100644
--- a/stix2patterns/v20/grammars/STIXPatternLexer.py
+++ b/stix2patterns/v20/grammars/STIXPatternLexer.py
@@ -1,4 +1,4 @@
-# Generated from STIXPattern.g4 by ANTLR 4.8
+# Generated from STIXPattern.g4 by ANTLR 4.7.1
 # encoding: utf-8
 from __future__ import print_function
 from antlr4 import *
@@ -6,7 +6,6 @@ from io import StringIO
 import sys
 
 
-
 def serializedATN():
     with StringIO() as buf:
         buf.write(u"\3\u608b\ua72a\u8133\ub9ed\u417c\u3be7\u7786\u5964\2")
@@ -343,7 +342,7 @@ class STIXPatternLexer(Lexer):
 
     def __init__(self, input=None, output=sys.stdout):
         super(STIXPatternLexer, self).__init__(input, output=output)
-        self.checkVersion("4.8")
+        self.checkVersion("4.7.1")
         self._interp = LexerATNSimulator(self, self.atn, self.decisionsToDFA, PredictionContextCache())
         self._actions = None
         self._predicates = None
diff --git a/stix2patterns/v20/grammars/STIXPatternListener.py b/stix2patterns/v20/grammars/STIXPatternListener.py
index af0b280..4af4467 100644
--- a/stix2patterns/v20/grammars/STIXPatternListener.py
+++ b/stix2patterns/v20/grammars/STIXPatternListener.py
@@ -1,4 +1,4 @@
-# Generated from STIXPattern.g4 by ANTLR 4.8
+# Generated from STIXPattern.g4 by ANTLR 4.7.1
 from antlr4 import *
 
 # This class defines a complete listener for a parse tree produced by STIXPatternParser.
diff --git a/stix2patterns/v20/grammars/STIXPatternParser.py b/stix2patterns/v20/grammars/STIXPatternParser.py
index 51174e9..65bbc4e 100644
--- a/stix2patterns/v20/grammars/STIXPatternParser.py
+++ b/stix2patterns/v20/grammars/STIXPatternParser.py
@@ -1,11 +1,10 @@
-# Generated from STIXPattern.g4 by ANTLR 4.8
+# Generated from STIXPattern.g4 by ANTLR 4.7.1
 # encoding: utf-8
 from __future__ import print_function
 from antlr4 import *
 from io import StringIO
 import sys
 
-
 def serializedATN():
     with StringIO() as buf:
         buf.write(u"\3\u608b\ua72a\u8133\ub9ed\u417c\u3be7\u7786\u5964\3")
@@ -226,13 +225,12 @@ class STIXPatternParser ( Parser ):
 
     def __init__(self, input, output=sys.stdout):
         super(STIXPatternParser, self).__init__(input, output=output)
-        self.checkVersion("4.8")
+        self.checkVersion("4.7.1")
         self._interp = ParserATNSimulator(self, self.atn, self.decisionsToDFA, self.sharedContextCache)
         self._predicates = None
 
 
 
-
     class PatternContext(ParserRuleContext):
 
         def __init__(self, parser, parent=None, invokingState=-1):
@@ -284,7 +282,6 @@ class STIXPatternParser ( Parser ):
             self.exitRule()
         return localctx
 
-
     class ObservationExpressionsContext(ParserRuleContext):
 
         def __init__(self, parser, parent=None, invokingState=-1):
@@ -366,7 +363,6 @@ class STIXPatternParser ( Parser ):
             self.unrollRecursionContexts(_parentctx)
         return localctx
 
-
     class ObservationExpressionOrContext(ParserRuleContext):
 
         def __init__(self, parser, parent=None, invokingState=-1):
@@ -448,7 +444,6 @@ class STIXPatternParser ( Parser ):
             self.unrollRecursionContexts(_parentctx)
         return localctx
 
-
     class ObservationExpressionAndContext(ParserRuleContext):
 
         def __init__(self, parser, parent=None, invokingState=-1):
@@ -530,7 +525,6 @@ class STIXPatternParser ( Parser ):
             self.unrollRecursionContexts(_parentctx)
         return localctx
 
-
     class ObservationExpressionContext(ParserRuleContext):
 
         def __init__(self, parser, parent=None, invokingState=-1):
@@ -785,7 +779,6 @@ class STIXPatternParser ( Parser ):
             self.unrollRecursionContexts(_parentctx)
         return localctx
 
-
     class ComparisonExpressionContext(ParserRuleContext):
 
         def __init__(self, parser, parent=None, invokingState=-1):
@@ -867,7 +860,6 @@ class STIXPatternParser ( Parser ):
             self.unrollRecursionContexts(_parentctx)
         return localctx
 
-
     class ComparisonExpressionAndContext(ParserRuleContext):
 
         def __init__(self, parser, parent=None, invokingState=-1):
@@ -949,7 +941,6 @@ class STIXPatternParser ( Parser ):
             self.unrollRecursionContexts(_parentctx)
         return localctx
 
-
     class PropTestContext(ParserRuleContext):
 
         def __init__(self, parser, parent=None, invokingState=-1):
@@ -1396,7 +1387,6 @@ class STIXPatternParser ( Parser ):
             self.exitRule()
         return localctx
 
-
     class StartStopQualifierContext(ParserRuleContext):
 
         def __init__(self, parser, parent=None, invokingState=-1):
@@ -1457,7 +1447,6 @@ class STIXPatternParser ( Parser ):
             self.exitRule()
         return localctx
 
-
     class WithinQualifierContext(ParserRuleContext):
 
         def __init__(self, parser, parent=None, invokingState=-1):
@@ -1522,7 +1511,6 @@ class STIXPatternParser ( Parser ):
             self.exitRule()
         return localctx
 
-
     class RepeatedQualifierContext(ParserRuleContext):
 
         def __init__(self, parser, parent=None, invokingState=-1):
@@ -1578,7 +1566,6 @@ class STIXPatternParser ( Parser ):
             self.exitRule()
         return localctx
 
-
     class ObjectPathContext(ParserRuleContext):
 
         def __init__(self, parser, parent=None, invokingState=-1):
@@ -1649,7 +1636,6 @@ class STIXPatternParser ( Parser ):
             self.exitRule()
         return localctx
 
-
     class ObjectTypeContext(ParserRuleContext):
 
         def __init__(self, parser, parent=None, invokingState=-1):
@@ -1704,7 +1690,6 @@ class STIXPatternParser ( Parser ):
             self.exitRule()
         return localctx
 
-
     class FirstPathComponentContext(ParserRuleContext):
 
         def __init__(self, parser, parent=None, invokingState=-1):
@@ -1759,7 +1744,6 @@ class STIXPatternParser ( Parser ):
             self.exitRule()
         return localctx
 
-
     class ObjectPathComponentContext(ParserRuleContext):
 
         def __init__(self, parser, parent=None, invokingState=-1):
@@ -1841,8 +1825,6 @@ class STIXPatternParser ( Parser ):
             super(STIXPatternParser.KeyPathStepContext, self).__init__(parser)
             self.copyFrom(ctx)
 
-        def DOT(self):
-            return self.getToken(STIXPatternParser.DOT, 0)
         def IdentifierWithoutHyphen(self):
             return self.getToken(STIXPatternParser.IdentifierWithoutHyphen, 0)
         def StringLiteral(self):
@@ -1940,7 +1922,6 @@ class STIXPatternParser ( Parser ):
             self.unrollRecursionContexts(_parentctx)
         return localctx
 
-
     class SetLiteralContext(ParserRuleContext):
 
         def __init__(self, parser, parent=None, invokingState=-1):
@@ -2034,7 +2015,6 @@ class STIXPatternParser ( Parser ):
             self.exitRule()
         return localctx
 
-
     class PrimitiveLiteralContext(ParserRuleContext):
 
         def __init__(self, parser, parent=None, invokingState=-1):
@@ -2097,7 +2077,6 @@ class STIXPatternParser ( Parser ):
             self.exitRule()
         return localctx
 
-
     class OrderableLiteralContext(ParserRuleContext):
 
         def __init__(self, parser, parent=None, invokingState=-1):
diff --git a/stix2patterns/v20/grammars/STIXPatternVisitor.py b/stix2patterns/v20/grammars/STIXPatternVisitor.py
index 177a3ed..78c84da 100644
--- a/stix2patterns/v20/grammars/STIXPatternVisitor.py
+++ b/stix2patterns/v20/grammars/STIXPatternVisitor.py
@@ -1,4 +1,4 @@
-# Generated from STIXPattern.g4 by ANTLR 4.8
+# Generated from STIXPattern.g4 by ANTLR 4.7.1
 from antlr4 import *
 
 # This class defines a complete generic visitor for a parse tree produced by STIXPatternParser.
diff --git a/stix2patterns/v20/pattern.py b/stix2patterns/v20/pattern.py
index 2603739..446e82c 100644
--- a/stix2patterns/v20/pattern.py
+++ b/stix2patterns/v20/pattern.py
@@ -40,15 +40,6 @@ class Pattern(object):
         subclass) instance."""
         antlr4.ParseTreeWalker.DEFAULT.walk(listener, self.__parse_tree)
 
-    def visit(self, visitor):
-        """
-        Walk the parse tree using the given visitor.
-
-        :param visitor: A visitor object (STIXPatternVisitor instance)
-        :return: The visitor's return value
-        """
-        return visitor.visit(self.__parse_tree)
-
     def __do_parse(self, pattern_str):
         """
         Parses the given pattern and returns the antlr parse tree.
diff --git a/stix2patterns/v21/grammars/STIXPatternLexer.py b/stix2patterns/v21/grammars/STIXPatternLexer.py
index bf1b059..e6c722e 100644
--- a/stix2patterns/v21/grammars/STIXPatternLexer.py
+++ b/stix2patterns/v21/grammars/STIXPatternLexer.py
@@ -1,4 +1,4 @@
-# Generated from STIXPattern.g4 by ANTLR 4.8
+# Generated from STIXPattern.g4 by ANTLR 4.7.1
 # encoding: utf-8
 from __future__ import print_function
 from antlr4 import *
@@ -6,7 +6,6 @@ from io import StringIO
 import sys
 
 
-
 def serializedATN():
     with StringIO() as buf:
         buf.write(u"\3\u608b\ua72a\u8133\ub9ed\u417c\u3be7\u7786\u5964\2")
@@ -347,7 +346,7 @@ class STIXPatternLexer(Lexer):
 
     def __init__(self, input=None, output=sys.stdout):
         super(STIXPatternLexer, self).__init__(input, output=output)
-        self.checkVersion("4.8")
+        self.checkVersion("4.7.1")
         self._interp = LexerATNSimulator(self, self.atn, self.decisionsToDFA, PredictionContextCache())
         self._actions = None
         self._predicates = None
diff --git a/stix2patterns/v21/grammars/STIXPatternListener.py b/stix2patterns/v21/grammars/STIXPatternListener.py
index 1b6c0ce..ad33415 100644
--- a/stix2patterns/v21/grammars/STIXPatternListener.py
+++ b/stix2patterns/v21/grammars/STIXPatternListener.py
@@ -1,4 +1,4 @@
-# Generated from STIXPattern.g4 by ANTLR 4.8
+# Generated from STIXPattern.g4 by ANTLR 4.7.1
 from antlr4 import *
 
 # This class defines a complete listener for a parse tree produced by STIXPatternParser.
diff --git a/stix2patterns/v21/grammars/STIXPatternParser.py b/stix2patterns/v21/grammars/STIXPatternParser.py
index f76d1f9..8e82c3b 100644
--- a/stix2patterns/v21/grammars/STIXPatternParser.py
+++ b/stix2patterns/v21/grammars/STIXPatternParser.py
@@ -1,11 +1,10 @@
-# Generated from STIXPattern.g4 by ANTLR 4.8
+# Generated from STIXPattern.g4 by ANTLR 4.7.1
 # encoding: utf-8
 from __future__ import print_function
 from antlr4 import *
 from io import StringIO
 import sys
 
-
 def serializedATN():
     with StringIO() as buf:
         buf.write(u"\3\u608b\ua72a\u8133\ub9ed\u417c\u3be7\u7786\u5964\3")
@@ -230,13 +229,12 @@ class STIXPatternParser ( Parser ):
 
     def __init__(self, input, output=sys.stdout):
         super(STIXPatternParser, self).__init__(input, output=output)
-        self.checkVersion("4.8")
+        self.checkVersion("4.7.1")
         self._interp = ParserATNSimulator(self, self.atn, self.decisionsToDFA, self.sharedContextCache)
         self._predicates = None
 
 
 
-
     class PatternContext(ParserRuleContext):
 
         def __init__(self, parser, parent=None, invokingState=-1):
@@ -288,7 +286,6 @@ class STIXPatternParser ( Parser ):
             self.exitRule()
         return localctx
 
-
     class ObservationExpressionsContext(ParserRuleContext):
 
         def __init__(self, parser, parent=None, invokingState=-1):
@@ -370,7 +367,6 @@ class STIXPatternParser ( Parser ):
             self.unrollRecursionContexts(_parentctx)
         return localctx
 
-
     class ObservationExpressionOrContext(ParserRuleContext):
 
         def __init__(self, parser, parent=None, invokingState=-1):
@@ -452,7 +448,6 @@ class STIXPatternParser ( Parser ):
             self.unrollRecursionContexts(_parentctx)
         return localctx
 
-
     class ObservationExpressionAndContext(ParserRuleContext):
 
         def __init__(self, parser, parent=None, invokingState=-1):
@@ -534,7 +529,6 @@ class STIXPatternParser ( Parser ):
             self.unrollRecursionContexts(_parentctx)
         return localctx
 
-
     class ObservationExpressionContext(ParserRuleContext):
 
         def __init__(self, parser, parent=None, invokingState=-1):
@@ -789,7 +783,6 @@ class STIXPatternParser ( Parser ):
             self.unrollRecursionContexts(_parentctx)
         return localctx
 
-
     class ComparisonExpressionContext(ParserRuleContext):
 
         def __init__(self, parser, parent=None, invokingState=-1):
@@ -871,7 +864,6 @@ class STIXPatternParser ( Parser ):
             self.unrollRecursionContexts(_parentctx)
         return localctx
 
-
     class ComparisonExpressionAndContext(ParserRuleContext):
 
         def __init__(self, parser, parent=None, invokingState=-1):
@@ -953,7 +945,6 @@ class STIXPatternParser ( Parser ):
             self.unrollRecursionContexts(_parentctx)
         return localctx
 
-
     class PropTestContext(ParserRuleContext):
 
         def __init__(self, parser, parent=None, invokingState=-1):
@@ -1436,7 +1427,6 @@ class STIXPatternParser ( Parser ):
             self.exitRule()
         return localctx
 
-
     class StartStopQualifierContext(ParserRuleContext):
 
         def __init__(self, parser, parent=None, invokingState=-1):
@@ -1497,7 +1487,6 @@ class STIXPatternParser ( Parser ):
             self.exitRule()
         return localctx
 
-
     class WithinQualifierContext(ParserRuleContext):
 
         def __init__(self, parser, parent=None, invokingState=-1):
@@ -1562,7 +1551,6 @@ class STIXPatternParser ( Parser ):
             self.exitRule()
         return localctx
 
-
     class RepeatedQualifierContext(ParserRuleContext):
 
         def __init__(self, parser, parent=None, invokingState=-1):
@@ -1618,7 +1606,6 @@ class STIXPatternParser ( Parser ):
             self.exitRule()
         return localctx
 
-
     class ObjectPathContext(ParserRuleContext):
 
         def __init__(self, parser, parent=None, invokingState=-1):
@@ -1688,7 +1675,6 @@ class STIXPatternParser ( Parser ):
             self.exitRule()
         return localctx
 
-
     class ObjectTypeContext(ParserRuleContext):
 
         def __init__(self, parser, parent=None, invokingState=-1):
@@ -1743,7 +1729,6 @@ class STIXPatternParser ( Parser ):
             self.exitRule()
         return localctx
 
-
     class FirstPathComponentContext(ParserRuleContext):
 
         def __init__(self, parser, parent=None, invokingState=-1):
@@ -1798,7 +1783,6 @@ class STIXPatternParser ( Parser ):
             self.exitRule()
         return localctx
 
-
     class ObjectPathComponentContext(ParserRuleContext):
 
         def __init__(self, parser, parent=None, invokingState=-1):
@@ -1880,8 +1864,6 @@ class STIXPatternParser ( Parser ):
             super(STIXPatternParser.KeyPathStepContext, self).__init__(parser)
             self.copyFrom(ctx)
 
-        def DOT(self):
-            return self.getToken(STIXPatternParser.DOT, 0)
         def IdentifierWithoutHyphen(self):
             return self.getToken(STIXPatternParser.IdentifierWithoutHyphen, 0)
         def StringLiteral(self):
@@ -1979,7 +1961,6 @@ class STIXPatternParser ( Parser ):
             self.unrollRecursionContexts(_parentctx)
         return localctx
 
-
     class SetLiteralContext(ParserRuleContext):
 
         def __init__(self, parser, parent=None, invokingState=-1):
@@ -2073,7 +2054,6 @@ class STIXPatternParser ( Parser ):
             self.exitRule()
         return localctx
 
-
     class PrimitiveLiteralContext(ParserRuleContext):
 
         def __init__(self, parser, parent=None, invokingState=-1):
@@ -2136,7 +2116,6 @@ class STIXPatternParser ( Parser ):
             self.exitRule()
         return localctx
 
-
     class OrderableLiteralContext(ParserRuleContext):
 
         def __init__(self, parser, parent=None, invokingState=-1):
diff --git a/stix2patterns/v21/grammars/STIXPatternVisitor.py b/stix2patterns/v21/grammars/STIXPatternVisitor.py
index 87423f5..87d7fe1 100644
--- a/stix2patterns/v21/grammars/STIXPatternVisitor.py
+++ b/stix2patterns/v21/grammars/STIXPatternVisitor.py
@@ -1,4 +1,4 @@
-# Generated from STIXPattern.g4 by ANTLR 4.8
+# Generated from STIXPattern.g4 by ANTLR 4.7.1
 from antlr4 import *
 
 # This class defines a complete generic visitor for a parse tree produced by STIXPatternParser.
diff --git a/stix2patterns/v21/pattern.py b/stix2patterns/v21/pattern.py
index 2603739..446e82c 100644
--- a/stix2patterns/v21/pattern.py
+++ b/stix2patterns/v21/pattern.py
@@ -40,15 +40,6 @@ class Pattern(object):
         subclass) instance."""
         antlr4.ParseTreeWalker.DEFAULT.walk(listener, self.__parse_tree)
 
-    def visit(self, visitor):
-        """
-        Walk the parse tree using the given visitor.
-
-        :param visitor: A visitor object (STIXPatternVisitor instance)
-        :return: The visitor's return value
-        """
-        return visitor.visit(self.__parse_tree)
-
     def __do_parse(self, pattern_str):
         """
         Parses the given pattern and returns the antlr parse tree.
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

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?
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'"
 ]
 
 

pip install -e .[dev]
parse_log_pytest
requirements.txt
pytest
3.9
dev-requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
alabaster==0.7.16
antlr4-python3-runtime==4.8
babel==2.17.0
build==1.2.2.post1
bump2version==1.0.1
bumpversion==0.6.0
cachetools==5.5.2
certifi==2025.1.31
cfgv==3.4.0
chardet==5.2.0
charset-normalizer==3.4.1
check-manifest==0.50
colorama==0.4.6
coverage==7.8.0
distlib==0.3.9
docutils==0.21.2
exceptiongroup==1.2.2
filelock==3.18.0
identify==2.6.9
idna==3.10
imagesize==1.4.1
importlib_metadata==8.6.1
iniconfig==2.1.0
Jinja2==3.1.6
MarkupSafe==3.0.2
nodeenv==1.9.1
packaging==24.2
platformdirs==4.3.7
pluggy==1.5.0
pre_commit==4.2.0
Pygments==2.19.1
pyproject-api==1.9.0
pyproject_hooks==1.2.0
pytest==8.3.5
pytest-cov==6.0.0
PyYAML==6.0.2
requests==2.32.3
six==1.17.0
snowballstemmer==2.2.0
Sphinx==7.4.7
sphinx-prompt==1.8.0
sphinxcontrib-applehelp==2.0.0
sphinxcontrib-devhelp==2.0.0
sphinxcontrib-htmlhelp==2.1.0
sphinxcontrib-jsmath==1.0.1
sphinxcontrib-qthelp==2.0.0
sphinxcontrib-serializinghtml==2.0.0
-e git+https://github.com/oasis-open/cti-pattern-validator.git@45f81f0c5b9481054ba28aea2ecb372faee7fe2b#egg=stix2_patterns
tomli==2.2.1
tox==4.25.0
typing_extensions==4.13.0
urllib3==2.3.0
virtualenv==20.29.3
zipp==3.21.0

name: cti-pattern-validator
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=25.0=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - alabaster==0.7.16
      - antlr4-python3-runtime==4.8
      - babel==2.17.0
      - build==1.2.2.post1
      - bump2version==1.0.1
      - bumpversion==0.6.0
      - cachetools==5.5.2
      - certifi==2025.1.31
      - cfgv==3.4.0
      - chardet==5.2.0
      - charset-normalizer==3.4.1
      - check-manifest==0.50
      - colorama==0.4.6
      - coverage==7.8.0
      - distlib==0.3.9
      - docutils==0.21.2
      - exceptiongroup==1.2.2
      - filelock==3.18.0
      - identify==2.6.9
      - idna==3.10
      - imagesize==1.4.1
      - importlib-metadata==8.6.1
      - iniconfig==2.1.0
      - jinja2==3.1.6
      - markupsafe==3.0.2
      - nodeenv==1.9.1
      - packaging==24.2
      - platformdirs==4.3.7
      - pluggy==1.5.0
      - pre-commit==4.2.0
      - pygments==2.19.1
      - pyproject-api==1.9.0
      - pyproject-hooks==1.2.0
      - pytest==8.3.5
      - pytest-cov==6.0.0
      - pyyaml==6.0.2
      - requests==2.32.3
      - six==1.17.0
      - snowballstemmer==2.2.0
      - sphinx==7.4.7
      - sphinx-prompt==1.8.0
      - sphinxcontrib-applehelp==2.0.0
      - sphinxcontrib-devhelp==2.0.0
      - sphinxcontrib-htmlhelp==2.1.0
      - sphinxcontrib-jsmath==1.0.1
      - sphinxcontrib-qthelp==2.0.0
      - sphinxcontrib-serializinghtml==2.0.0
      - tomli==2.2.1
      - tox==4.25.0
      - typing-extensions==4.13.0
      - urllib3==2.3.0
      - virtualenv==20.29.3
      - zipp==3.21.0
prefix: /opt/conda/envs/cti-pattern-validator

stix2patterns/test/v21/test_validator.py::test_fail_patterns[[win-registry-key:key stix2patterns/test/v21/test_validator.py::test_fail_patterns[([win-registry-key:key stix2patterns/test/v21/test_validator.py::test_pass_patterns[([win-registry-key:key stix2patterns/test/v21/test_validator.py::test_pass_patterns[[network-traffic:src_port
stix2patterns/test/v21/test_validator.py::test_spec_patterns[[file:hashes.'SHA-256' stix2patterns/test/v21/test_validator.py::test_spec_patterns[[email-message:from_ref.value stix2patterns/test/v21/test_validator.py::test_spec_patterns[([file:hashes.MD5 stix2patterns/test/v21/test_validator.py::test_spec_patterns[[user-account:account_type stix2patterns/test/v21/test_validator.py::test_spec_patterns[[artifact:mime_type stix2patterns/test/v21/test_validator.py::test_spec_patterns[[file:name stix2patterns/test/v21/test_validator.py::test_spec_patterns[[file:extensions.'windows-pebinary-ext'.sections[*].entropy stix2patterns/test/v21/test_validator.py::test_spec_patterns[[file:mime_type stix2patterns/test/v21/test_validator.py::test_spec_patterns[[network-traffic:dst_ref.type stix2patterns/test/v21/test_validator.py::test_spec_patterns[[domain-name:value stix2patterns/test/v21/test_validator.py::test_spec_patterns[[url:value stix2patterns/test/v21/test_validator.py::test_spec_patterns[[x509-certificate:issuer stix2patterns/test/v21/test_validator.py::test_spec_patterns[[windows-registry-key:key stix2patterns/test/v21/test_validator.py::test_spec_patterns[[(file:name stix2patterns/test/v21/test_validator.py::test_spec_patterns[[email-message:sender_ref.value stix2patterns/test/v21/test_validator.py::test_spec_patterns[[x-usb-device:usbdrive.serial_number stix2patterns/test/v21/test_validator.py::test_spec_patterns[[process:command_line stix2patterns/test/v21/test_validator.py::test_spec_patterns[[network-traffic:dst_ref.value stix2patterns/test/v21/test_validator.py::test_spec_patterns[([file:name stix2patterns/test/v21/test_validator.py::test_spec_patterns[[file:hashes.MD5 stix2patterns/test/v21/test_validator.py::test_fail_patterns[file:size stix2patterns/test/v21/test_validator.py::test_fail_patterns[[file:size stix2patterns/test/v21/test_validator.py::test_fail_patterns[[file:hashes.MD5 stix2patterns/test/v21/test_validator.py::test_fail_patterns[[file.size stix2patterns/test/v21/test_validator.py::test_fail_patterns[[file:name stix2patterns/test/v21/test_validator.py::test_fail_patterns[[network-traffic:dst_ref.value stix2patterns/test/v21/test_validator.py::test_fail_patterns[[x_whatever:detected stix2patterns/test/v21/test_validator.py::test_fail_patterns[[artifact:payload_bin stix2patterns/test/v21/test_validator.py::test_fail_patterns[[foo:bar=1] stix2patterns/test/v21/test_validator.py::test_fail_patterns[[file:hashes.'SHA-256' stix2patterns/test/v21/test_validator.py::test_pass_patterns[[file:size stix2patterns/test/v21/test_validator.py::test_pass_patterns[[file:file_name stix2patterns/test/v21/test_validator.py::test_pass_patterns[[file:extended_properties.'ntfs-ext'.sid stix2patterns/test/v21/test_validator.py::test_pass_patterns[[emailaddr:value stix2patterns/test/v21/test_validator.py::test_pass_patterns[[ipv4addr:value stix2patterns/test/v21/test_validator.py::test_pass_patterns[[user-account:value stix2patterns/test/v21/test_validator.py::test_pass_patterns[[file:file_system_properties.file_name stix2patterns/test/v21/test_validator.py::test_pass_patterns[[network-connection:extended_properties[0].source_payload stix2patterns/test/v21/test_validator.py::test_pass_patterns[[win-registry-key:key stix2patterns/test/v21/test_validator.py::test_pass_patterns[[x_whatever:detected stix2patterns/test/v21/test_validator.py::test_pass_patterns[[artifact:payload_bin stix2patterns/test/v21/test_validator.py::test_pass_patterns[[foo:bar=1] stix2patterns/test/v21/test_validator.py::test_pass_patterns[[network-traffic:start stix2patterns/test/v21/test_validator.py::test_pass_patterns[(
stix2patterns/v20/pattern.py stix2patterns/v21/grammars/STIXPatternListener.py setup.py stix2patterns/v20/grammars/STIXPatternLexer.py stix2patterns/v21/grammars/STIXPatternParser.py stix2patterns/v20/grammars/STIXPatternListener.py .travis.yml stix2patterns/v21/pattern.py stix2patterns/v20/grammars/STIXPatternParser.py stix2patterns/v21/grammars/STIXPatternLexer.py stix2patterns/v21/validator.py stix2patterns/v21/grammars/STIXPatternVisitor.py stix2patterns/v20/grammars/STIXPatternVisitor.py
stix2patterns/v20/pattern.py stix2patterns/v21/grammars/STIXPatternListener.py setup.py stix2patterns/v20/grammars/STIXPatternLexer.py stix2patterns/v21/grammars/STIXPatternParser.py stix2patterns/v20/grammars/STIXPatternListener.py .travis.yml stix2patterns/v21/pattern.py stix2patterns/v20/grammars/STIXPatternParser.py stix2patterns/v21/grammars/STIXPatternLexer.py stix2patterns/v21/validator.py stix2patterns/v21/grammars/STIXPatternVisitor.py stix2patterns/v20/grammars/STIXPatternVisitor.py