Repository: oasis-open/cti-stix-elevator

Issue:
The disable/enable option are not handled correctly.
The use case is: programmatically enable/disable messages at any time.

Because of the way the enable/disable option works, using set_option_value will not work as expected
A user would have to know that the “real” option is disabled, not disable

There also seems to be an issue with the processing of the enable/disable option.  They are strings from the command line, but CHECK_CODES has integers.

Discussion:

Patch:
diff --git a/idioms-json-2.1-valid/Appendix_G_IOCs_Full.json b/idioms-json-2.1-valid/Appendix_G_IOCs_Full.json
index b6c60fb..2f62f74 100644
--- a/idioms-json-2.1-valid/Appendix_G_IOCs_Full.json
+++ b/idioms-json-2.1-valid/Appendix_G_IOCs_Full.json
@@ -1451,4 +1451,4 @@
         }
     ],
     "type": "bundle"
-}
+}
\ No newline at end of file
diff --git a/stix2elevator/cli.py b/stix2elevator/cli.py
index eaa19c6..feb35ee 100644
--- a/stix2elevator/cli.py
+++ b/stix2elevator/cli.py
@@ -107,8 +107,8 @@ def _get_arg_parser(is_script=True):
         help="A comma-separated list of the stix2-elevator messages to enable. "
              "If the --disable option is not used, no other messages will be "
              "shown. \n\nExample: stix2_elevator.py <file> --enable 250",
-        dest="enable",
-        default=""
+        dest="enabled",
+        default=None
     )
 
     parser.add_argument(
@@ -116,8 +116,8 @@ def _get_arg_parser(is_script=True):
         "--disable",
         help="A comma-separated list of the stix2-elevator messages to disable. \n\n"
              "Example: stix2_elevator.py <file> --disable 212,220",
-        dest="disable",
-        default=""
+        dest="disabled",
+        default=None
     )
 
     parser.add_argument(
diff --git a/stix2elevator/options.py b/stix2elevator/options.py
index 86f4a1a..8ff2377 100644
--- a/stix2elevator/options.py
+++ b/stix2elevator/options.py
@@ -1,8 +1,9 @@
+import copy
 import logging
 import os
 import shlex
 
-from six import text_type
+from six import string_types
 from stix2validator.scripts import stix2_validator
 
 ALL_OPTIONS = None
@@ -86,6 +87,29 @@ def setup_logger(package_id):
         log.addHandler(fh)
 
 
+def _convert_to_int_list(check_codes):
+    """Takes a comma-separated string or list of strings and converts to list of ints.
+
+    Args:
+        check_codes: comma-separated string or list of strings
+
+    Returns:
+        list: the check codes as a list of integers
+
+    Raises:
+        ValueError: if conversion fails
+        RuntimeError: if cannot determine how to convert input
+    """
+    if isinstance(check_codes, list):
+        if all(isinstance(x, int) for x in check_codes):
+            return check_codes  # good input
+        else:
+            return [int(x) for x in check_codes]  # list of str
+    elif isinstance(check_codes, string_types):
+        return [int(x) for x in check_codes.split(",")]  # str, comma-separated expected
+    raise RuntimeError("Could not convert values: {} of type {}".format(check_codes, type(check_codes)))
+
+
 class ElevatorOptions(object):
     """Collection of stix2-elevator options which can be set via command line or
     programmatically in a script.
@@ -107,8 +131,8 @@ class ElevatorOptions(object):
             When this value is not set, current time will be used instead.
         validator_args: If set, these values will be used to create a
             ValidationOptions instance if requested.  The elevator should not produce any custom objects.
-        enable: Messages to enable.
-        disable: Messages to disable.
+        enabled: Messages to enable. Expects a list of ints.
+        disabled: Messages to disable. Expects a list of ints.
         silent: If set, no stix2-elevator log messages will be emitted.
         message_log_directory: If set, it will write all emitted messages to
             file. It will use the filename or package id to name the log file.
@@ -119,7 +143,7 @@ class ElevatorOptions(object):
     def __init__(self, cmd_args=None, file_=None, incidents=False,
                  missing_policy="add-to-description", custom_property_prefix="elevator",
                  infrastructure=False, package_created_by_id=None, default_timestamp=None,
-                 validator_args="--strict-types", enable="", disable="",
+                 validator_args="--strict-types", enabled=None, disabled=None,
                  silent=False, message_log_directory=None,
                  policy="no_policy", output_directory=None, log_level="INFO",
                  markings_allowed="", spec_version="2.0"):
@@ -135,8 +159,8 @@ class ElevatorOptions(object):
             self.default_timestamp = cmd_args.default_timestamp
             self.validator_args = cmd_args.validator_args
 
-            self.enable = cmd_args.enable
-            self.disable = cmd_args.disable
+            self.enabled = cmd_args.enabled
+            self.disabled = cmd_args.disabled
             self.silent = cmd_args.silent
             self.policy = cmd_args.policy
             self.message_log_directory = cmd_args.message_log_directory
@@ -144,9 +168,6 @@ class ElevatorOptions(object):
             self.markings_allowed = cmd_args.markings_allowed
             if hasattr(cmd_args, "output_directory"):
                 self.output_directory = cmd_args.output_directory
-            # validator arg --silent is currently broken
-            # if self.silent:
-            #    self.validator_args += " --silent"
             self.spec_version = cmd_args.spec_version
 
         else:
@@ -159,8 +180,8 @@ class ElevatorOptions(object):
             self.default_timestamp = default_timestamp
             self.validator_args = validator_args
 
-            self.enable = enable
-            self.disable = disable
+            self.enabled = enabled
+            self.disabled = disabled
             self.silent = silent
             self.policy = policy
             self.message_log_directory = message_log_directory
@@ -172,33 +193,65 @@ class ElevatorOptions(object):
         if self.validator_args.find("--version") == -1:
             self.validator_args = self.validator_args + " --version " + self.spec_version
 
-        # Convert string of comma-separated checks to a list,
-        # and convert check code numbers to names. By default all messages are
-        # enabled.
-        if self.disable:
-            self.disabled = self.disable.split(",")
-            self.disabled = [CHECK_CODES[x] if x in CHECK_CODES else x
-                             for x in self.disabled]
-        else:
-            self.disabled = []
-
-        if self.enable:
-            self.enabled = self.enable.split(",")
-            self.enabled = [CHECK_CODES[x] if x in CHECK_CODES else x
-                            for x in self.enabled]
-        else:
-            self.enabled = [text_type(x) for x in CHECK_CODES]
-
         if self.markings_allowed:
             self.markings_allowed = self.markings_allowed.split(",")
 
         self.marking_container = None
 
+    @property
+    def disabled(self):
+        return self._disabled
+
+    @disabled.setter
+    def disabled(self, disabled):
+        def remove_silent(item, elements):
+            try:
+                elements.remove(item)
+            except ValueError:
+                pass  # suppress exception if value is not present
+        # Convert string of comma-separated checks to a list,
+        # and convert check code numbers to names. By default no messages are
+        # disabled.
+        if disabled:
+            self._disabled = _convert_to_int_list(disabled)
+            self._disabled = [x for x in self._disabled if x in CHECK_CODES]
+            for x in self._disabled:
+                remove_silent(x, self._enabled)
+        else:
+            self._disabled = []
+
+    @property
+    def enabled(self):
+        return self._enabled
+
+    @enabled.setter
+    def enabled(self, enabled):
+        def remove_silent(item, elements):
+            try:
+                elements.remove(item)
+            except ValueError:
+                pass  # suppress exception if value is not present
+        # Convert string of comma-separated checks to a list,
+        # and convert check code numbers to names. By default all messages are
+        # enabled.
+        if enabled:
+            self._enabled = _convert_to_int_list(enabled)
+            self._enabled = [x for x in self._enabled if x in CHECK_CODES]
+            for x in self._enabled:
+                remove_silent(x, self._disabled)
+        else:
+            self._enabled = copy.deepcopy(CHECK_CODES)
+
 
-def initialize_options(**kwargs):
+def initialize_options(options=None):
     global ALL_OPTIONS
     if not ALL_OPTIONS:
-        ALL_OPTIONS = ElevatorOptions(**kwargs)
+        if isinstance(options, ElevatorOptions):
+            ALL_OPTIONS = options
+        elif isinstance(options, dict):
+            ALL_OPTIONS = ElevatorOptions(**options)
+        else:
+            ALL_OPTIONS = ElevatorOptions(options)
 
         if ALL_OPTIONS.silent and ALL_OPTIONS.message_log_directory:
             warn("Both console and output log have disabled messages.", 209)
@@ -232,8 +285,6 @@ def set_option_value(option_name, option_value):
 
 
 def msg_id_enabled(msg_id):
-    msg_id = text_type(msg_id)
-
     if get_option_value("silent"):
         return False
 


Test patch:
diff --git a/stix2elevator/test/test_main.py b/stix2elevator/test/test_main.py
index a962429..8b97a01 100644
--- a/stix2elevator/test/test_main.py
+++ b/stix2elevator/test/test_main.py
@@ -1,3 +1,4 @@
+from argparse import Namespace
 import io
 import os
 
@@ -9,8 +10,12 @@ import pytest
 from stix2elevator import (elevate,
                            elevate_file,
                            elevate_package,
-                           elevate_string)
-from stix2elevator.options import initialize_options, set_option_value
+                           elevate_string,
+                           options)
+from stix2elevator.options import (ElevatorOptions,
+                                   get_option_value,
+                                   initialize_options,
+                                   set_option_value)
 from stix2elevator.utils import find_dir
 
 # This module only tests for the main functions used to interact with the elevator from a programmatic or
@@ -29,6 +34,24 @@ def setup_options():
     set_option_value("policy", "no_policy")
 
 
+@pytest.mark.parametrize("opts", [
+    ElevatorOptions(policy="no_policy", spec_version="2.1", log_level="DEBUG", disabled=[212, 901]),
+    {"policy": "no_policy", "spec_version": "2.1", "log_level": "DEBUG", "disabled": [212, 901]},
+    Namespace(policy="no_policy", spec_version="2.1", log_level="DEBUG", disabled="212,901",
+              file_=None, incidents=False, missing_policy="add-to-description",
+              custom_property_prefix="elevator", infrastructure=False, package_created_by_id=None,
+              default_timestamp=None, validator_args="--strict-types", enabled=None, silent=False,
+              message_log_directory=None, output_directory=None, markings_allowed=""),
+])
+def test_setup_options(opts):
+    options.ALL_OPTIONS = None  # To make sure we can set it again
+    initialize_options(opts)
+    assert get_option_value("policy") == "no_policy"
+    assert get_option_value("spec_version") == "2.1"
+    assert get_option_value("log_level") == "DEBUG"
+    assert get_option_value("disabled") == [212, 901]
+
+
 def test_elevate_with_marking_container():
     setup_options()
 
diff --git a/stix2elevator/test/test_utils.py b/stix2elevator/test/test_utils.py
index 93c6aa3..bb1eea2 100644
--- a/stix2elevator/test/test_utils.py
+++ b/stix2elevator/test/test_utils.py
@@ -1,6 +1,8 @@
 from stix.indicator import Indicator
 
+import pytest
 from stix2elevator import convert_stix, utils
+from stix2elevator.options import _convert_to_int_list
 from stix2elevator.utils import Environment
 
 
@@ -26,3 +28,23 @@ def test_convert_timestamp_string():
     indicator = Indicator()
     indicator_instance = convert_stix.create_basic_object("indicator", indicator, env)
     assert indicator_instance is not None
+
+
+@pytest.mark.parametrize("data", [
+    [123, 245, 344],
+    "123,245,344",
+    ["123", "245", 344],
+])
+def test_convert_int_function(data):
+    assert _convert_to_int_list(data) == [123, 245, 344]
+
+
+@pytest.mark.parametrize("data", [
+    "12 3,245,344",
+    "212,garbage,33",
+    "definitely-not,an_int",
+    234,
+])
+def test_convert_int_function_bad(data):
+    with pytest.raises((RuntimeError, ValueError)):
+        _convert_to_int_list(data)