Repository: oasis-open/cti-stix-validator
Pull request #201 (2022-11-17 15:51:18)

Allow custom schemas to use extension def ids
This lets you use multiple extensions for the same object type, or extensions that extend multiple object types.


Patch:
diff --git a/stix2validator/validator.py b/stix2validator/validator.py
index 6bc533b..d848252 100644
--- a/stix2validator/validator.py
+++ b/stix2validator/validator.py
@@ -569,11 +569,11 @@ def load_validator(schema_path, schema):
     return validator
 
 
-def find_schema(schema_dir, obj_type):
-    """Search the `schema_dir` directory for a schema called `obj_type`.json.
+def find_schema(schema_dir, name):
+    """Search the `schema_dir` directory for a schema called `name`.json.
     Return the file path of the first match it finds.
     """
-    schema_filename = obj_type + '.json'
+    schema_filename = name + '.json'
 
     for root, dirnames, filenames in os.walk(schema_dir):
         if "examples" in root:
@@ -602,11 +602,11 @@ def load_schema(schema_path):
     return schema
 
 
-def _get_error_generator(type, obj, schema_dir=None, version=DEFAULT_VER, default='core'):
+def _get_error_generator(name, obj, schema_dir=None, version=DEFAULT_VER, default='core'):
     """Get a generator for validating against the schema for the given object type.
 
     Args:
-        type (str): The object type to find the schema for.
+        name (str): The object type or extension id to find the schema for.
         obj: The object to be validated.
         schema_dir (str): The path in which to search for schemas.
         version (str): The version of the STIX specification to validate
@@ -628,7 +628,7 @@ def _get_error_generator(type, obj, schema_dir=None, version=DEFAULT_VER, defaul
                                      + version + '/schemas/')
 
     try:
-        schema_path = find_schema(schema_dir, type)
+        schema_path = find_schema(schema_dir, name)
         schema = load_schema(schema_path)
     except (KeyError, TypeError):
         # Assume a custom object with no schema
@@ -643,6 +643,7 @@ def _get_error_generator(type, obj, schema_dir=None, version=DEFAULT_VER, defaul
                 raise SchemaInvalidError("Cannot locate a schema for the object's "
                                          "type, nor the base schema ({}.json).".format(default))
 
+    type = obj['type']
     if type == 'observed-data' and schema_dir is None:
         # Validate against schemas for specific observed data object types later.
         # If schema_dir is not None the schema is custom and won't need to be modified.
@@ -771,12 +772,20 @@ def _schema_validate(obj, options, bundle_version=None):
         if interop_errors:
             error_gens.append((interop_errors, error_prefix))
 
-    # Get validator for any user-supplied schema
+    # Get validators for any user-supplied schema
     if options.schema_dir:
+        # schemas named by object type
         custom_sdo_errors = _get_error_generator(obj['type'], obj, options.schema_dir, version, default=core_schema)
         if custom_sdo_errors:
             error_gens.append((custom_sdo_errors, error_prefix))
 
+        # schemas for extensions
+        extension_ids = obj.get('extensions', dict()).keys()
+        for ext in extension_ids:
+            extension_errors = _get_error_generator(ext, obj, options.schema_dir, version, default=core_schema)
+            if extension_errors:
+                error_gens.append((extension_errors, error_prefix))
+
     # Validate each cyber observable object separately
     if obj['type'] == 'observed-data' and 'objects' in obj:
         # Check if observed data property is in dictionary format


Test patch:
diff --git a/stix2validator/test/v21/indicator_tests.py b/stix2validator/test/v21/indicator_tests.py
index 36205d4..d6ad046 100644
--- a/stix2validator/test/v21/indicator_tests.py
+++ b/stix2validator/test/v21/indicator_tests.py
@@ -204,14 +204,28 @@ class IndicatorTestCases(ValidatorTest):
         indicator['pattern'] = "[windows-registry-key:key LIKE 'HKEY_LOCAL_MACHINE\\\\Foo\\\\Bar%']"
         self.assertTrueWithOptions(indicator)
 
-    def test_additional_schemas(self):
+    def test_additional_schema(self):
         indicator = copy.deepcopy(self.valid_indicator)
         self.assertFalseWithOptions(indicator, schema_dir=self.custom_schemas)
 
         indicator['name'] = "Foobar"
         self.assertTrueWithOptions(indicator, schema_dir=self.custom_schemas)
 
-    def test_additional_schemas_custom_type(self):
+    def test_additional_schema_extension(self):
+        indicator = copy.deepcopy(self.valid_indicator)
+        indicator['name'] = "Foobar"
+        indicator['extensions'] = {'extension-definition--993ce403-6fdd-4efa-8279-31cf2d91b1c8': {
+            "extension_type": "toplevel-property-extension",
+        }}
+        self.assertFalseWithOptions(indicator, schema_dir=self.custom_schemas)
+
+        indicator['grade'] = "a"
+        self.assertFalseWithOptions(indicator, schema_dir=self.custom_schemas)
+
+        indicator['grade'] = "A"
+        self.assertTrueWithOptions(indicator, schema_dir=self.custom_schemas)
+
+    def test_additional_schema_custom_type(self):
         # no schema exists for this type
         new_obj = {
             "type": "x-type",
@@ -240,7 +254,7 @@ class IndicatorTestCases(ValidatorTest):
         new_obj['property2'] = 10
         self.assertTrueWithOptions(new_obj, schema_dir=self.custom_schemas)
 
-    def test_additional_schemas_custom_type_invalid_schema(self):
+    def test_additional_schema_custom_type_invalid_schema(self):
         self.assertFalseWithOptions(ADDTNL_INVALID_SCHEMA, schema_dir=self.custom_schemas)
 
     def test_validate_parsed_json_list_additional_invalid_schema(self):
diff --git a/stix2validator/test/v21/test_schemas/extension-definition--993ce403-6fdd-4efa-8279-31cf2d91b1c8.json b/stix2validator/test/v21/test_schemas/extension-definition--993ce403-6fdd-4efa-8279-31cf2d91b1c8.json
new file mode 100644
index 0000000..8f2ea9e
--- /dev/null
+++ b/stix2validator/test/v21/test_schemas/extension-definition--993ce403-6fdd-4efa-8279-31cf2d91b1c8.json
@@ -0,0 +1,26 @@
+{
+    "properties": {
+        "grade": {
+            "type": "string",
+            "pattern": "^[ABCDF]$"
+        },
+        "extensions": {
+            "patternProperties": {
+                "^extension-definition--993ce403-6fdd-4efa-8279-31cf2d91b1c8$": {
+                    "type": "object",
+                    "properties": {
+                        "extension_type": {
+                           "type": "string",
+                            "enum": ["toplevel-property-extension"]
+                        }
+                    },
+                    "required": ["extension_type"]
+                }
+            }
+        }
+    },
+    "required": [
+        "grade",
+        "extensions"
+    ]
+}