Repository: oasis-open/cti-stix-validator (python)

Issue:
the validator should be able to be called as a library method
The elevator "calls" the stix2 validator as sort of a library.  The elevator should just use the validator via its API (command line args).  Currently, the elevator is parsing the validator's args and setting some of them directly in ValidatorOptions - which should be hidden.

The main method should be rewritten, so you just pass in the command line arg string.  Then the new main method of the validator is just a wrapper around that library method.

Pull request:
Improve using the validator as a library
Fixes #52 - use `parse_args` to get a `ValidationOptions` class to pass to one of the validation functions.

Patch:
diff --git a/stix2validator/__init__.py b/stix2validator/__init__.py
index d4303da..19f43de 100644
--- a/stix2validator/__init__.py
+++ b/stix2validator/__init__.py
@@ -6,7 +6,7 @@ import sys
 
 from .errors import ValidationError
 from .output import print_results
-from .util import ValidationOptions
+from .util import ValidationOptions, parse_args
 from .validator import (run_validation, validate, validate_file,
                         validate_parsed_json, validate_string)
 from .version import __version__
diff --git a/stix2validator/scripts/stix2_validator.py b/stix2validator/scripts/stix2_validator.py
index c7ff7dd..a3d1631 100644
--- a/stix2validator/scripts/stix2_validator.py
+++ b/stix2validator/scripts/stix2_validator.py
@@ -3,316 +3,26 @@
 """Validate STIX 2.0 documents against the specification.
 """
 
-import argparse
-from argparse import RawDescriptionHelpFormatter
-import datetime
-import errno
 import logging
-import os
 import sys
-import textwrap
 
-from appdirs import AppDirs
-import requests_cache
-
-from stix2validator import (ValidationError, ValidationOptions, codes, output,
+from stix2validator import (ValidationError, codes, output, parse_args,
                             print_results, run_validation)
 
-CODES_TABLE = """
-The following is a table of all the recommended "best practice" checks which
-the validator performs, along with the code to use with the --enable or
---disable options. By default, the validator checks all of them.
-
-+------+-----------------------------+----------------------------------------+
-| Code | Name                        | Ensures...                             |
-+------+-----------------------------+----------------------------------------+
-|  1   | format-checks               | all 1xx checks are run                 |
-| 101  | custom-prefix               | names of custom object types,          |
-|      |                             | properties, observable objects,        |
-|      |                             | observable object properties, and      |
-|      |                             | observable object extensions follow    |
-|      |                             | the correct format                     |
-| 102  | custom-prefix-lax           | same as 101 but more lenient; no       |
-|      |                             | source identifier needed in prefix     |
-| 111  | open-vocab-format           | values of open vocabularies follow the |
-|      |                             | correct format                         |
-| 121  | kill-chain-names            | kill-chain-phase name and phase follow |
-|      |                             | the correct format                     |
-| 141  | observable-object-keys      | observable object keys follow the      |
-|      |                             | correct format                         |
-| 142  | observable-dictionary-keys  | dictionaries in cyber observable       |
-|      |                             | objects follow the correct format      |
-| 149  | windows-process-priority-   | windows-process-ext's 'priority'       |
-|      |     format                  | follows the correct format             |
-| 150  | hash-length                 | keys in 'hashes'-type properties are   |
-|      |                             | not too long                           |
-|      |                             |                                        |
-|  2   | approved-values             | all 2xx checks are run                 |
-| 201  | marking-definition-type     | marking definitions use a valid        |
-|      |                             | definition_type                        |
-| 202  | relationship-types          | relationships are among those defined  |
-|      |                             | in the specification                   |
-| 203  | duplicate-ids               | objects in a bundle with duplicate IDs |
-|      |                             | have different `modified` timestamps   |
-| 210  | all-vocabs                  | all of the following open vocabulary   |
-|      |                             | checks are run                         |
-| 211  | attack-motivation           | certain property values are from the   |
-|      |                             | attack_motivation vocabulary           |
-| 212  | attack-resource-level       | certain property values are from the   |
-|      |                             | attack_resource_level vocabulary       |
-| 213  | identity-class              | certain property values are from the   |
-|      |                             | identity_class vocabulary              |
-| 214  | indicator-label             | certain property values are from the   |
-|      |                             | indicator_label vocabulary             |
-| 215  | industry-sector             | certain property values are from the   |
-|      |                             | industry_sector vocabulary             |
-| 216  | malware-label               | certain property values are from the   |
-|      |                             | malware_label vocabulary               |
-| 218  | report-label                | certain property values are from the   |
-|      |                             | report_label vocabulary                |
-| 219  | threat-actor-label          | certain property values are from the   |
-|      |                             | threat_actor_label vocabulary          |
-| 220  | threat-actor-role           | certain property values are from the   |
-|      |                             | threat_actor_role vocabulary           |
-| 221  | threat-actor-sophistication | certain property values are from the   |
-|      |                             | threat_actor_sophistication vocabulary |
-| 222  | tool-label                  | certain property values are from the   |
-|      |                             | tool_label vocabulary                  |
-| 241  | hash-algo                   | certain property values are from the   |
-|      |                             | hash-algo vocabulary                   |
-| 242  | encryption-algo             | certain property values are from the   |
-|      |                             | encryption-algo vocabulary             |
-| 243  | windows-pebinary-type       | certain property values are from the   |
-|      |                             | windows-pebinary-type vocabulary       |
-| 244  | account-type                | certain property values are from the   |
-|      |                             | account-type vocabulary                |
-| 270  | all-external-sources        | all of the following external source   |
-|      |                             | checks are run                         |
-| 271  | mime-type                   | file.mime_type is a valid IANA MIME    |
-|      |                             | type                                   |
-| 272  | protocols                   | certain property values are valid IANA |
-|      |                             | Service and Protocol names             |
-| 273  | ipfix                       | certain property values are valid IANA |
-|      |                             | IP Flow Information Export (IPFIX)     |
-|      |                             | Entities                               |
-| 274  | http-request-headers        | certain property values are valid HTTP |
-|      |                             | request header names                   |
-| 275  | socket-options              | certain property values are valid      |
-|      |                             | socket options                         |
-| 276  | pdf-doc-info                | certain property values are valid PDF  |
-|      |                             | Document Information Dictionary keys   |
-| 301  | network-traffic-ports       | network-traffic objects contain both   |
-|      |                             | src_port and dst_port                  |
-| 302  | extref-hashes               | external references SHOULD have hashes |
-|      |                             | if they have a url                     |
-+------+-----------------------------+----------------------------------------+
-"""
-
 logger = logging.getLogger(__name__)
 
 
-class NewlinesHelpFormatter(RawDescriptionHelpFormatter):
-    """Custom help formatter to insert newlines between argument help texts.
-    """
-    def _split_lines(self, text, width):
-        text = self._whitespace_matcher.sub(' ', text).strip()
-        txt = textwrap.wrap(text, width)
-        txt[-1] += '\n'
-        return txt
-
-
-def _get_arg_parser(is_script=True):
-    """Initializes and returns an argparse.ArgumentParser instance for this
-    application.
-
-    Args:
-        is_script: Whether the arguments are intended for use in a stand-alone
-            script or imported into another tool.
-
-    Returns:
-        Instance of ``argparse.ArgumentParser``
-
-    """
-    parser = argparse.ArgumentParser(
-        description=__doc__,
-        formatter_class=NewlinesHelpFormatter,
-        epilog=CODES_TABLE
-    )
-
-    # Input options
-    if is_script:
-        parser.add_argument(
-            "files",
-            metavar="FILES",
-            nargs="+",
-            help="A whitespace separated list of STIX files or directories of "
-                 "STIX files to validate."
-        )
-    parser.add_argument(
-        "-r",
-        "--recursive",
-        dest="recursive",
-        action="store_true",
-        default=True,
-        help="Recursively descend into input directories."
-    )
-    parser.add_argument(
-        "-s",
-        "--schemas",
-        dest="schema_dir",
-        help="Custom schema directory. If provided, input will be validated "
-             "against these schemas in addition to the STIX schemas bundled "
-             "with this script."
-    )
-
-    # Output options
-    parser.add_argument(
-        "-v",
-        "--verbose",
-        dest="verbose",
-        action="store_true",
-        default=False,
-        help="Print informational notes and more verbose error messages."
-    )
-
-    parser.add_argument(
-        "-q",
-        "--silent",
-        dest="silent",
-        action="store_true",
-        default=False,
-        help="Silence all output to stdout."
-    )
-
-    parser.add_argument(
-        "-d",
-        "--disable",
-        "--ignore",
-        dest="disabled",
-        default="",
-        help="A comma-separated list of recommended best practice checks to "
-             "skip. By default, no checks are disabled. \n\n"
-             "Example: --disable 212,220"
-    )
-
-    parser.add_argument(
-        "-e",
-        "--enable",
-        "--select",
-        dest="enabled",
-        default="",
-        help="A comma-separated list of recommended best practice checks to "
-             "enable. If the --disable option is not used, no other checks "
-             "will be run. By default, all checks are enabled.\n\n"
-             "Example: --enable 250"
-    )
-
-    parser.add_argument(
-        "--strict",
-        dest="strict",
-        action="store_true",
-        default=False,
-        help="Treat warnings as errors and fail validation if any are found."
-    )
-
-    parser.add_argument(
-        "--strict-types",
-        dest="strict_types",
-        action="store_true",
-        default=False,
-        help="Ensure that no custom object types are used, only those defined"
-             " in the STIX specification."
-    )
-
-    parser.add_argument(
-        "--strict-properties",
-        dest="strict_properties",
-        action="store_true",
-        default=False,
-        help="Ensure that no custom properties are used, only those defined"
-             " in the STIX specification."
-    )
-
-    parser.add_argument(
-        "--no-cache",
-        dest="no_cache",
-        action="store_true",
-        default=False,
-        help="Disable the caching of external source values."
-    )
-
-    parser.add_argument(
-        "--refresh-cache",
-        dest="refresh_cache",
-        action="store_true",
-        default=False,
-        help="Clears the cache of external source values, then "
-             "during validation downloads them again."
-    )
-
-    parser.add_argument(
-        "--clear-cache",
-        dest="clear_cache",
-        action="store_true",
-        default=False,
-        help="Clear the cache of external source values after validation."
-    )
-
-    return parser
-
-
-def init_requests_cache(refresh_cache=False):
-    """
-    Initializes a cache which the ``requests`` library will consult for
-    responses, before making network requests.
-
-    :param refresh_cache: Whether the cache should be cleared out
-    """
-    # Cache data from external sources; used in some checks
-    dirs = AppDirs("stix2-validator", "OASIS")
-    # Create cache dir if doesn't exist
-    try:
-        os.makedirs(dirs.user_cache_dir)
-    except OSError as e:
-        if e.errno != errno.EEXIST:
-            raise
-    requests_cache.install_cache(
-        cache_name=os.path.join(dirs.user_cache_dir, 'py{}cache'.format(
-            sys.version_info[0])),
-        expire_after=datetime.timedelta(weeks=1))
-
-    if refresh_cache:
-        clear_requests_cache()
-
-
-def clear_requests_cache():
-    """
-    Clears all cached responses.
-    """
-    now = datetime.datetime.utcnow()
-    requests_cache.get_cache().remove_old_entries(now)
-
-
 def main():
     # Parse command line arguments
-    parser = _get_arg_parser()
-    args = parser.parse_args()
-
-    options = ValidationOptions(args)
+    options = parse_args(sys.argv[1:], is_script=True)
 
     try:
-        if not options.no_cache:
-            init_requests_cache(options.refresh_cache)
-
         # Validate input documents
         results = run_validation(options)
 
         # Print validation results
         print_results(results)
 
-        if not options.no_cache and options.clear_cache:
-            clear_requests_cache()
-
         # Determine exit status code and exit.
         code = codes.get_code(results)
         sys.exit(code)
diff --git a/stix2validator/util.py b/stix2validator/util.py
index 47f8632..eab52cb 100644
--- a/stix2validator/util.py
+++ b/stix2validator/util.py
@@ -1,7 +1,258 @@
+import argparse
+from argparse import RawDescriptionHelpFormatter
 from collections import Iterable
+import datetime
+import errno
+import os
+import sys
+import textwrap
+
+from appdirs import AppDirs
+import requests_cache
 
 from .output import error, set_level, set_silent
 
+CODES_TABLE = """
+The following is a table of all the recommended "best practice" checks which
+the validator performs, along with the code to use with the --enable or
+--disable options. By default, the validator checks all of them.
+
++------+-----------------------------+----------------------------------------+
+| Code | Name                        | Ensures...                             |
++------+-----------------------------+----------------------------------------+
+|  1   | format-checks               | all 1xx checks are run                 |
+| 101  | custom-prefix               | names of custom object types,          |
+|      |                             | properties, observable objects,        |
+|      |                             | observable object properties, and      |
+|      |                             | observable object extensions follow    |
+|      |                             | the correct format                     |
+| 102  | custom-prefix-lax           | same as 101 but more lenient; no       |
+|      |                             | source identifier needed in prefix     |
+| 111  | open-vocab-format           | values of open vocabularies follow the |
+|      |                             | correct format                         |
+| 121  | kill-chain-names            | kill-chain-phase name and phase follow |
+|      |                             | the correct format                     |
+| 141  | observable-object-keys      | observable object keys follow the      |
+|      |                             | correct format                         |
+| 142  | observable-dictionary-keys  | dictionaries in cyber observable       |
+|      |                             | objects follow the correct format      |
+| 149  | windows-process-priority-   | windows-process-ext's 'priority'       |
+|      |     format                  | follows the correct format             |
+| 150  | hash-length                 | keys in 'hashes'-type properties are   |
+|      |                             | not too long                           |
+|      |                             |                                        |
+|  2   | approved-values             | all 2xx checks are run                 |
+| 201  | marking-definition-type     | marking definitions use a valid        |
+|      |                             | definition_type                        |
+| 202  | relationship-types          | relationships are among those defined  |
+|      |                             | in the specification                   |
+| 203  | duplicate-ids               | objects in a bundle with duplicate IDs |
+|      |                             | have different `modified` timestamps   |
+| 210  | all-vocabs                  | all of the following open vocabulary   |
+|      |                             | checks are run                         |
+| 211  | attack-motivation           | certain property values are from the   |
+|      |                             | attack_motivation vocabulary           |
+| 212  | attack-resource-level       | certain property values are from the   |
+|      |                             | attack_resource_level vocabulary       |
+| 213  | identity-class              | certain property values are from the   |
+|      |                             | identity_class vocabulary              |
+| 214  | indicator-label             | certain property values are from the   |
+|      |                             | indicator_label vocabulary             |
+| 215  | industry-sector             | certain property values are from the   |
+|      |                             | industry_sector vocabulary             |
+| 216  | malware-label               | certain property values are from the   |
+|      |                             | malware_label vocabulary               |
+| 218  | report-label                | certain property values are from the   |
+|      |                             | report_label vocabulary                |
+| 219  | threat-actor-label          | certain property values are from the   |
+|      |                             | threat_actor_label vocabulary          |
+| 220  | threat-actor-role           | certain property values are from the   |
+|      |                             | threat_actor_role vocabulary           |
+| 221  | threat-actor-sophistication | certain property values are from the   |
+|      |                             | threat_actor_sophistication vocabulary |
+| 222  | tool-label                  | certain property values are from the   |
+|      |                             | tool_label vocabulary                  |
+| 241  | hash-algo                   | certain property values are from the   |
+|      |                             | hash-algo vocabulary                   |
+| 242  | encryption-algo             | certain property values are from the   |
+|      |                             | encryption-algo vocabulary             |
+| 243  | windows-pebinary-type       | certain property values are from the   |
+|      |                             | windows-pebinary-type vocabulary       |
+| 244  | account-type                | certain property values are from the   |
+|      |                             | account-type vocabulary                |
+| 270  | all-external-sources        | all of the following external source   |
+|      |                             | checks are run                         |
+| 271  | mime-type                   | file.mime_type is a valid IANA MIME    |
+|      |                             | type                                   |
+| 272  | protocols                   | certain property values are valid IANA |
+|      |                             | Service and Protocol names             |
+| 273  | ipfix                       | certain property values are valid IANA |
+|      |                             | IP Flow Information Export (IPFIX)     |
+|      |                             | Entities                               |
+| 274  | http-request-headers        | certain property values are valid HTTP |
+|      |                             | request header names                   |
+| 275  | socket-options              | certain property values are valid      |
+|      |                             | socket options                         |
+| 276  | pdf-doc-info                | certain property values are valid PDF  |
+|      |                             | Document Information Dictionary keys   |
+| 301  | network-traffic-ports       | network-traffic objects contain both   |
+|      |                             | src_port and dst_port                  |
+| 302  | extref-hashes               | external references SHOULD have hashes |
+|      |                             | if they have a url                     |
++------+-----------------------------+----------------------------------------+
+"""
+
+
+class NewlinesHelpFormatter(RawDescriptionHelpFormatter):
+    """Custom help formatter to insert newlines between argument help texts.
+    """
+    def _split_lines(self, text, width):
+        text = self._whitespace_matcher.sub(' ', text).strip()
+        txt = textwrap.wrap(text, width)
+        txt[-1] += '\n'
+        return txt
+
+
+def parse_args(cmd_args, is_script=False):
+    """Parses a list of command line arguments into a ValidationOptions object.
+
+    Args:
+        cmd_args (list of str): The list of command line arguments to be parsed.
+        is_script: Whether the arguments are intended for use in a stand-alone
+            script or imported into another tool.
+
+    Returns:
+        Instance of ``ValidationOptions``
+
+    """
+    parser = argparse.ArgumentParser(
+        description=__doc__,
+        formatter_class=NewlinesHelpFormatter,
+        epilog=CODES_TABLE
+    )
+
+    # Input options
+    if is_script:
+        parser.add_argument(
+            "files",
+            metavar="FILES",
+            nargs="+",
+            help="A whitespace separated list of STIX files or directories of "
+                 "STIX files to validate."
+        )
+    parser.add_argument(
+        "-r",
+        "--recursive",
+        dest="recursive",
+        action="store_true",
+        default=True,
+        help="Recursively descend into input directories."
+    )
+    parser.add_argument(
+        "-s",
+        "--schemas",
+        dest="schema_dir",
+        help="Custom schema directory. If provided, input will be validated "
+             "against these schemas in addition to the STIX schemas bundled "
+             "with this script."
+    )
+
+    # Output options
+    parser.add_argument(
+        "-v",
+        "--verbose",
+        dest="verbose",
+        action="store_true",
+        default=False,
+        help="Print informational notes and more verbose error messages."
+    )
+
+    parser.add_argument(
+        "-q",
+        "--silent",
+        dest="silent",
+        action="store_true",
+        default=False,
+        help="Silence all output to stdout."
+    )
+
+    parser.add_argument(
+        "-d",
+        "--disable",
+        "--ignore",
+        dest="disabled",
+        default="",
+        help="A comma-separated list of recommended best practice checks to "
+             "skip. By default, no checks are disabled. \n\n"
+             "Example: --disable 202,210"
+    )
+
+    parser.add_argument(
+        "-e",
+        "--enable",
+        "--select",
+        dest="enabled",
+        default="",
+        help="A comma-separated list of recommended best practice checks to "
+             "enable. If the --disable option is not used, no other checks "
+             "will be run. By default, all checks are enabled.\n\n"
+             "Example: --enable 218"
+    )
+
+    parser.add_argument(
+        "--strict",
+        dest="strict",
+        action="store_true",
+        default=False,
+        help="Treat warnings as errors and fail validation if any are found."
+    )
+
+    parser.add_argument(
+        "--strict-types",
+        dest="strict_types",
+        action="store_true",
+        default=False,
+        help="Ensure that no custom object types are used, only those defined"
+             " in the STIX specification."
+    )
+
+    parser.add_argument(
+        "--strict-properties",
+        dest="strict_properties",
+        action="store_true",
+        default=False,
+        help="Ensure that no custom properties are used, only those defined"
+             " in the STIX specification."
+    )
+
+    parser.add_argument(
+        "--no-cache",
+        dest="no_cache",
+        action="store_true",
+        default=False,
+        help="Disable the caching of external source values."
+    )
+
+    parser.add_argument(
+        "--refresh-cache",
+        dest="refresh_cache",
+        action="store_true",
+        default=False,
+        help="Clears the cache of external source values, then "
+             "during validation downloads them again."
+    )
+
+    parser.add_argument(
+        "--clear-cache",
+        dest="clear_cache",
+        action="store_true",
+        default=False,
+        help="Clear the cache of external source values after validation."
+    )
+
+    args = parser.parse_args(cmd_args)
+    return ValidationOptions(args)
+
 
 class ValidationOptions(object):
     """Collection of validation options which can be set via command line or
@@ -162,3 +413,35 @@ def cyber_observable_check(original_function):
                 yield x
     new_function.__name__ = original_function.__name__
     return new_function
+
+
+def init_requests_cache(refresh_cache=False):
+    """
+    Initializes a cache which the ``requests`` library will consult for
+    responses, before making network requests.
+
+    :param refresh_cache: Whether the cache should be cleared out
+    """
+    # Cache data from external sources; used in some checks
+    dirs = AppDirs("stix2-validator", "OASIS")
+    # Create cache dir if doesn't exist
+    try:
+        os.makedirs(dirs.user_cache_dir)
+    except OSError as e:
+        if e.errno != errno.EEXIST:
+            raise
+    requests_cache.install_cache(
+        cache_name=os.path.join(dirs.user_cache_dir, 'py{}cache'.format(
+            sys.version_info[0])),
+        expire_after=datetime.timedelta(weeks=1))
+
+    if refresh_cache:
+        clear_requests_cache()
+
+
+def clear_requests_cache():
+    """
+    Clears all cached responses.
+    """
+    now = datetime.datetime.utcnow()
+    requests_cache.get_cache().remove_old_entries(now)
diff --git a/stix2validator/validator.py b/stix2validator/validator.py
index 19cddcb..1d1ad16 100644
--- a/stix2validator/validator.py
+++ b/stix2validator/validator.py
@@ -14,7 +14,7 @@ from six import iteritems, string_types, text_type
 from . import musts, output, shoulds
 from .errors import (NoJSONFileFoundError, SchemaError, SchemaInvalidError,
                      ValidationError, pretty_error)
-from .util import ValidationOptions
+from .util import ValidationOptions, clear_requests_cache, init_requests_cache
 
 DEFAULT_SCHEMA_DIR = os.path.abspath(os.path.dirname(__file__) + '/schemas/')
 
@@ -360,6 +360,9 @@ def validate_parsed_json(obj_json, options=None):
     if not options:
         options = ValidationOptions()
 
+    if not options.no_cache:
+        init_requests_cache(options.refresh_cache)
+
     results = None
     try:
         if validating_list:
@@ -380,6 +383,9 @@ def validate_parsed_json(obj_json, options=None):
         else:
             results = error_result
 
+    if not options.no_cache and options.clear_cache:
+        clear_requests_cache()
+
     return results
 
 


Test patch:
diff --git a/stix2validator/test/util_tests.py b/stix2validator/test/util_tests.py
new file mode 100644
index 0000000..079587f
--- /dev/null
+++ b/stix2validator/test/util_tests.py
@@ -0,0 +1,36 @@
+from .. import parse_args
+
+
+def test_parse_args():
+    args = [
+        '-r',
+        '-s',
+        '/tmp/schemas/',
+        '-q',
+        '-d',
+        'format-checks',
+        '-e',
+        'custom-prefix',
+        '--strict',
+        '--strict-types',
+        '--strict-properties',
+        '--no-cache',
+        '--refresh-cache',
+        '--clear-cache',
+        '/tmp/mystix.json',
+    ]
+    options = parse_args(args, True)
+
+    assert options.verbose is False
+    assert options.silent is True
+    assert options.files == ['/tmp/mystix.json']
+    assert options.recursive is True
+    assert options.schema_dir == '/tmp/schemas/'
+    assert options.disabled == ['format-checks']
+    assert options.enabled == ['custom-prefix']
+    assert options.strict is True
+    assert options.strict_types is True
+    assert options.strict_properties is True
+    assert options.no_cache is True
+    assert options.refresh_cache is True
+    assert options.clear_cache is True