diff --git a/.github/workflows/python-ci-tests.yml b/.github/workflows/python-ci-tests.yml index 6539334a..0859e4e2 100644 --- a/.github/workflows/python-ci-tests.yml +++ b/.github/workflows/python-ci-tests.yml @@ -6,10 +6,20 @@ on: [push, pull_request] jobs: build: - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} strategy: matrix: - python-version: [3.8, 3.9, '3.10', '3.11', '3.12'] + os: [ubuntu-latest, windows-latest] + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] + exclude: + - os: windows-latest + python-version: '3.8' + - os: windows-latest + python-version: '3.9' + - os: windows-latest + python-version: '3.10' + - os: windows-latest + python-version: '3.11' name: Python ${{ matrix.python-version }} Build steps: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 35b2d48b..5646f2a0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,7 +6,7 @@ repos: exclude: ^stix2validator/(v20|v21)/assets/.*.csv$ - id: check-merge-conflict - repo: https://github.com/PyCQA/flake8 - rev: 3.8.4 + rev: 5.0.4 hooks: - id: flake8 name: Check project styling diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 00000000..59cacfd0 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,24 @@ +# Read the Docs configuration file for Sphinx projects +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +# Set the OS, Python version and other tools you might need +build: + os: ubuntu-22.04 + tools: + python: "3.12" + +# Build documentation in the "docs/" directory with Sphinx +sphinx: + configuration: docs/conf.py + +# Build all formats (incl. pdf, epub) +formats: all + +# Declare the Python requirements required to build your documentation +# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html +python: + install: + - requirements: requirements.txt diff --git a/CHANGELOG b/CHANGELOG index f072385b..31806929 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,22 @@ CHANGELOG ========= +3.2.0 - 2024-04-05 + +* Updated jsonschema dependency version, removed deprecated refResolver + (@ostefano) +* Enforce requirement that network-traffic.http-request-ext.request_header is a + list of strings instead of a singular string +* Allow -ext extensions in SDOs +* Fixed bug when loading files containing unicode characters on certain + platforms +* Fixed bug with duplicate log entries when importing the validator script as a + library (@ostefano) +* Switched to including IETF data as package data instead of pulling and + caching locally, removed caching options, and removed requests-cache and + appdirs dependencies (@vEpiphyte) +* Dropped support for Python 3.7 + 3.1.4 - 2023-07-24 * Allow latest attrs package to fix community reported dependency problems diff --git a/docs/conf.py b/docs/conf.py index 53ce291f..6fe5ce61 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -10,8 +10,8 @@ copyright = '2018-2022, OASIS Open' author = 'OASIS Open' -version = '3.1.4' -release = '3.1.4' +version = '3.2.0' +release = '3.2.0' language = None exclude_patterns = ['_build', '_templates', 'Thumbs.db', '.DS_Store'] diff --git a/setup.cfg b/setup.cfg index d673316b..86c0b9d4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 3.1.4 +current_version = 3.2.0 commit = True tag = True diff --git a/setup.py b/setup.py index 8ab4d9a2..ec25d30a 100644 --- a/setup.py +++ b/setup.py @@ -19,8 +19,9 @@ def get_version(): install_requires = [ 'colorama', 'cpe', - 'jsonschema[format-nongpl]>=4.6.0,<4.18.0', + 'jsonschema[format-nongpl]>=4.20.0', 'python-dateutil', + 'requests', 'simplejson', 'stix2-patterns>=0.4.1', ] diff --git a/stix2validator/schemas-2.1 b/stix2validator/schemas-2.1 index 9d86da9b..818be8a2 160000 --- a/stix2validator/schemas-2.1 +++ b/stix2validator/schemas-2.1 @@ -1 +1 @@ -Subproject commit 9d86da9b3a5c5416fa4fbd92c76d891662a77401 +Subproject commit 818be8a25a635ea696c115bdf7126e300a606717 diff --git a/stix2validator/scripts/stix2_validator.py b/stix2validator/scripts/stix2_validator.py index e81ffd6b..0b488647 100644 --- a/stix2validator/scripts/stix2_validator.py +++ b/stix2validator/scripts/stix2_validator.py @@ -10,17 +10,18 @@ from stix2validator import (ValidationError, codes, output, parse_args, print_results, run_validation) -logging.basicConfig(stream=sys.stdout, level=logging.INFO, format='%(message)s') -logger = logging.getLogger(__name__) - def main(): # Parse command line arguments options = parse_args(sys.argv[1:], is_script=True) + # Initialize the logger + logging.basicConfig(stream=sys.stdout, level=logging.INFO, format='%(message)s') + logger = logging.getLogger(__name__) + # Only print prompt if script is run on cmdline and no input is piped in if options.files == sys.stdin and os.isatty(0): - logging.info('Input STIX content, then press Ctrl+D: ') + logger.info('Input STIX content, then press Ctrl+D: ') try: # Validate input documents diff --git a/stix2validator/test/v20/misc_tests.py b/stix2validator/test/v20/misc_tests.py index 8852e87a..dd03ae68 100644 --- a/stix2validator/test/v20/misc_tests.py +++ b/stix2validator/test/v20/misc_tests.py @@ -41,7 +41,7 @@ def test_run_validation(caplog): def test_run_validation_nonexistent_file(): - options = ValidationOptions(files='asdf.json', version="2.0") + options = ValidationOptions(files=['asdf.json'], version="2.0") with pytest.raises(NoJSONFileFoundError): run_validation(options) diff --git a/stix2validator/test/v21/misc_tests.py b/stix2validator/test/v21/misc_tests.py index 5451675c..09597a7a 100644 --- a/stix2validator/test/v21/misc_tests.py +++ b/stix2validator/test/v21/misc_tests.py @@ -16,10 +16,20 @@ EXAMPLE = os.path.join(os.path.dirname(os.path.realpath(__file__)), '..', '..', 'schemas-2.1', 'examples', 'indicator-to-campaign-relationship.json') +CUSTOM = os.path.join(os.path.dirname(os.path.realpath(__file__)), + 'test_examples', 'tlp-amber.json') +CUSTOM_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), + 'test_schemas') +RELATIVE = os.path.join(os.path.dirname(os.path.realpath(__file__)), + 'test_examples', 'tool.json') +RELATIVE_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), + 'test_schemas') IDENTITY = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'test_examples', 'identity.json') IDENTITY_CUSTOM = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'test_examples', 'identity_custom.json') +IDENTITY_UNICODE = os.path.join(os.path.dirname(os.path.realpath(__file__)), + 'test_examples', 'identity_unicode.json') INVALID_BRACES = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'test_examples', 'invalid_braces.json') INVALID_COMMA = os.path.join(os.path.dirname(os.path.realpath(__file__)), @@ -63,6 +73,24 @@ def test_validate_file(caplog): assert 'STIX JSON: Valid' in caplog.text +def test_validate_file_custom(caplog): + caplog.set_level('INFO') + results = validate_file(CUSTOM, options=ValidationOptions(schema_dir=CUSTOM_DIR)) + assert results.is_valid + + print_results(results) + assert 'STIX JSON: Valid' in caplog.text + + +def test_validate_file_custom_relative(caplog): + caplog.set_level('INFO') + results = validate_file(RELATIVE, options=ValidationOptions(schema_dir=RELATIVE_DIR)) + assert results.is_valid + + print_results(results) + assert 'STIX JSON: Valid' in caplog.text + + def test_validate_file_warning(caplog): results = validate_file(IDENTITY_CUSTOM) assert results.is_valid @@ -71,6 +99,11 @@ def test_validate_file_warning(caplog): assert re.search("Custom property .+ should ", caplog.text) +def test_validate_file_unicode(caplog): + results = validate_file(IDENTITY_UNICODE) + assert results.is_valid + + def test_validate_file_invalid_brace(caplog): results = validate_file(INVALID_BRACES) assert not results.is_valid diff --git a/stix2validator/test/v21/network_traffic_tests.py b/stix2validator/test/v21/network_traffic_tests.py index 5fc0c839..ecee1dc7 100644 --- a/stix2validator/test/v21/network_traffic_tests.py +++ b/stix2validator/test/v21/network_traffic_tests.py @@ -82,9 +82,9 @@ def test_network_traffic_http_request_header(self): "request_value": "/download.html", "request_version": "http/1.1", "request_header": { - "Accept-Encoding": "gzip,deflate", - "Host": "www.example.com", - "x-foobar": "something" + "Accept-Encoding": ["gzip,deflate"], + "Host": ["www.example.com"], + "x-foobar": ["something"] } } } diff --git a/stix2validator/test/v21/test_examples/identity_unicode.json b/stix2validator/test/v21/test_examples/identity_unicode.json new file mode 100644 index 00000000..ae3c9a29 --- /dev/null +++ b/stix2validator/test/v21/test_examples/identity_unicode.json @@ -0,0 +1,9 @@ +{ + "type": "identity", + "spec_version": "2.1", + "id": "identity--8c6af861-7b20-41ef-9b59-6344fd872a8f", + "created": "2016-08-08T15:50:10.983Z", + "modified": "2016-08-08T15:50:10.983Z", + "name": "Heizölrückstoßabdämpfung", + "identity_class": "organization" +} diff --git a/stix2validator/test/v21/test_examples/tlp-amber.json b/stix2validator/test/v21/test_examples/tlp-amber.json new file mode 100644 index 00000000..26f04331 --- /dev/null +++ b/stix2validator/test/v21/test_examples/tlp-amber.json @@ -0,0 +1,19 @@ +{ + "type": "bundle", + "id": "bundle--63ab8e67-acac-4817-845a-d09f0e86954c", + "objects": [ + { + "type": "marking-definition", + "spec_version": "2.1", + "id": "marking-definition--55d920b0-5e8b-4f79-9ee9-91f868d9b421", + "created": "2022-10-01T00:00:00.000Z", + "name": "TLP:AMBER", + "extensions": { + "extension-definition--60a3c5c5-0d10-413e-aab3-9e08dde9e88d": { + "extension_type": "property-extension", + "tlp_2_0" : "amber" + } + } + } + ] +} \ No newline at end of file diff --git a/stix2validator/test/v21/test_examples/tool.json b/stix2validator/test/v21/test_examples/tool.json new file mode 100644 index 00000000..8ab167a0 --- /dev/null +++ b/stix2validator/test/v21/test_examples/tool.json @@ -0,0 +1,12 @@ +{ + "type": "tool", + "spec_version": "2.1", + "id": "tool--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", + "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + "created": "2016-04-06T20:03:48.000Z", + "modified": "2016-04-06T20:03:48.000Z", + "tool_types": [ "remote-access"], + "name": "VNC", + "foo_value": "bizz", + "bar_value": "buzz" +} diff --git a/stix2validator/test/v21/test_schemas/bar.json b/stix2validator/test/v21/test_schemas/bar.json new file mode 100644 index 00000000..1c7d57f2 --- /dev/null +++ b/stix2validator/test/v21/test_schemas/bar.json @@ -0,0 +1,7 @@ +{ + "properties": { + "bar_value": { + "type": "string" + } + } +} diff --git a/stix2validator/test/v21/test_schemas/extension-definition--60a3c5c5-0d10-413e-aab3-9e08dde9e88d.json b/stix2validator/test/v21/test_schemas/extension-definition--60a3c5c5-0d10-413e-aab3-9e08dde9e88d.json new file mode 100644 index 00000000..2e9a945f --- /dev/null +++ b/stix2validator/test/v21/test_schemas/extension-definition--60a3c5c5-0d10-413e-aab3-9e08dde9e88d.json @@ -0,0 +1,322 @@ +{ + "$id": "tlp2.0.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "tlp2.0-marking-definition-extension", + "description": "This marking extension was created to apply TLP2.0 data markings", + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "The type of this object, which MUST be the literal `marking-definition`.", + "enum": [ + "marking-definition" + ] + }, + "spec_version": { + "type": "string", + "enum": [ + "2.1" + ], + "description": "The version of the STIX specification used to represent this object." + }, + "created_by_ref": { + "$ref": "http://raw.githubusercontent.com/oasis-open/cti-stix2-json-schemas/stix2.1/schemas/common/identifier.json", + "description": "The created_by_ref property specifies the ID of the identity object that describes the entity that created this Marking Definition." + }, + "created": { + "$ref": "http://raw.githubusercontent.com/oasis-open/cti-stix2-json-schemas/stix2.1/schemas/common/timestamp.json", + "description": "The created property represents the time at which the first version of this Marking Definition object was created." + }, + "external_references": { + "type": "array", + "description": "A list of external references which refers to non-STIX information.", + "items": { + "$ref": "http://raw.githubusercontent.com/oasis-open/cti-stix2-json-schemas/stix2.1/schemas/common/external-reference.json" + }, + "minItems": 1 + }, + "object_marking_refs": { + "type": "array", + "description": "The object_marking_refs property specifies a list of IDs of marking-definition objects that apply to this Marking Definition.", + "items": { + "allOf": [ + { + "$ref": "http://raw.githubusercontent.com/oasis-open/cti-stix2-json-schemas/stix2.1/schemas/common/identifier.json" + }, + { + "pattern": "^marking-definition--" + } + ] + }, + "minItems": 1 + }, + "granular_markings": { + "type": "array", + "description": "The granular_markings property specifies a list of granular markings applied to this object.", + "items": { + "$ref": "http://raw.githubusercontent.com/oasis-open/cti-stix2-json-schemas/stix2.1/schemas/common/granular-marking.json" + }, + "minItems": 1 + } + }, + "oneOf": [ + { + "$ref": "#/definitions/tlp_clear" + }, + { + "$ref": "#/definitions/tlp_green" + }, + { + "$ref": "#/definitions/tlp_amber" + }, + { + "$ref": "#/definitions/tlp_amber_strict" + }, + { + "$ref": "#/definitions/tlp_red" + } + ], + "required": [ + "id", + "type", + "name", + "spec_version", + "created", + "extensions" + ], + "definitions": { + "tlp_clear": { + "description": "The marking-definition object representing Traffic Light Protocol (TLP) Clear.", + "properties": { + "id": { + "type": "string", + "enum": [ + "marking-definition--94868c89-83c2-464b-929b-a1a8aa3c8487" + ] + }, + "name": { + "type": "string", + "enum": [ + "TLP:CLEAR" + ] + }, + "extensions": { + "type": "object", + "properties": { + "extension-definition--60a3c5c5-0d10-413e-aab3-9e08dde9e88d": { + "type": "object", + "properties": { + "extension_type": { + "type": "string", + "description": "Defined by STIX 2.1 extension definition rules from 'extension-type-enum'.", + "enum": [ + "property-extension" + ] + }, + "tlp_2_0": { + "type": "string", + "enum": [ + "clear" + ] + } + }, + "required": [ + "extension_type", + "tlp_2_0" + ], + "additionalProperties": false + } + }, + "required": [ + "extension-definition--60a3c5c5-0d10-413e-aab3-9e08dde9e88d" + ], + "additionalProperties": false + } + } + }, + "tlp_green": { + "description": "The marking-definition object representing Traffic Light Protocol (TLP) Green.", + "properties": { + "id": { + "type": "string", + "enum": [ + "marking-definition--bab4a63c-aed9-4cf5-a766-dfca5abac2bb" + ] + }, + "name": { + "type": "string", + "enum": [ + "TLP:GREEN" + ] + }, + "extensions": { + "type": "object", + "properties": { + "extension-definition--60a3c5c5-0d10-413e-aab3-9e08dde9e88d": { + "type": "object", + "properties": { + "extension_type": { + "type": "string", + "description": "Defined by STIX 2.1 extension definition rules from 'extension-type-enum'.", + "enum": [ + "property-extension" + ] + }, + "tlp_2_0": { + "type": "string", + "enum": [ + "green" + ] + } + }, + "required": [ + "extension_type", + "tlp_2_0" + ], + "additionalProperties": false + } + }, + "additionalProperties": false + } + } + }, + "tlp_amber": { + "description": "The marking-definition object representing Traffic Light Protocol (TLP) Amber.", + "properties": { + "id": { + "type": "string", + "enum": [ + "marking-definition--55d920b0-5e8b-4f79-9ee9-91f868d9b421" + ] + }, + "name": { + "type": "string", + "enum": [ + "TLP:AMBER" + ] + }, + "extensions": { + "type": "object", + "properties": { + "extension-definition--60a3c5c5-0d10-413e-aab3-9e08dde9e88d": { + "type": "object", + "properties": { + "extension_type": { + "type": "string", + "description": "Defined by STIX 2.1 extension definition rules from 'extension-type-enum'.", + "enum": [ + "property-extension" + ] + }, + "tlp_2_0": { + "type": "string", + "enum": [ + "amber" + ] + } + }, + "required": [ + "extension_type", + "tlp_2_0" + ], + "additionalProperties": false + } + }, + "additionalProperties": false + } + } + }, + "tlp_amber_strict": { + "description": "The marking-definition object representing Traffic Light Protocol (TLP) Amber+Strict.", + "properties": { + "id": { + "type": "string", + "enum": [ + "marking-definition--939a9414-2ddd-4d32-a0cd-375ea402b003" + ] + }, + "name": { + "type": "string", + "enum": [ + "TLP:AMBER+STRICT" + ] + }, + "extensions": { + "type": "object", + "properties": { + "extension-definition--60a3c5c5-0d10-413e-aab3-9e08dde9e88d": { + "type": "object", + "properties": { + "extension_type": { + "type": "string", + "description": "Defined by STIX 2.1 extension definition rules from 'extension-type-enum'.", + "enum": [ + "property-extension" + ] + }, + "tlp_2_0": { + "type": "string", + "enum": [ + "amber+strict" + ] + } + }, + "required": [ + "extension_type", + "tlp_2_0" + ], + "additionalProperties": false + } + }, + "additionalProperties": false + } + } + }, + "tlp_red": { + "description": "The marking-definition object representing Traffic Light Protocol (TLP) Red.", + "properties": { + "id": { + "type": "string", + "enum": [ + "marking-definition--e828b379-4e03-4974-9ac4-e53a884c97c1" + ] + }, + "name": { + "type": "string", + "enum": [ + "TLP:RED" + ] + }, + "extensions": { + "type": "object", + "properties": { + "extension-definition--60a3c5c5-0d10-413e-aab3-9e08dde9e88d": { + "type": "object", + "properties": { + "extension_type": { + "type": "string", + "description": "Defined by STIX 2.1 extension definition rules from 'extension-type-enum'.", + "enum": [ + "property-extension" + ] + }, + "tlp_2_0": { + "type": "string", + "enum": [ + "red" + ] + } + }, + "required": [ + "extension_type", + "tlp_2_0" + ], + "additionalProperties": false + } + }, + "additionalProperties": false + } + } + } + } +} \ No newline at end of file diff --git a/stix2validator/test/v21/test_schemas/tool.json b/stix2validator/test/v21/test_schemas/tool.json new file mode 100644 index 00000000..01672d65 --- /dev/null +++ b/stix2validator/test/v21/test_schemas/tool.json @@ -0,0 +1,15 @@ +{ + "allOf": [ + { + "properties": { + "foo_value": { + "type": "string" + } + }, + "required": ["foo_value"] + }, + { + "$ref": "bar.json" + } + ] +} diff --git a/stix2validator/validator.py b/stix2validator/validator.py index 186b4596..d2e63e23 100644 --- a/stix2validator/validator.py +++ b/stix2validator/validator.py @@ -2,15 +2,22 @@ """ from collections.abc import Iterable +import copy +import functools import io from itertools import chain import os +import pathlib import re import sys +from urllib import parse, request -from jsonschema import Draft202012Validator, RefResolver +from jsonschema import Draft202012Validator from jsonschema import exceptions as schema_exceptions from jsonschema.validators import extend +from referencing import Registry, Resource +from referencing.jsonschema import DRAFT202012 +import requests import simplejson as json from . import output @@ -448,7 +455,7 @@ def validate_file(fn, options=None): options = ValidationOptions(files=fn) try: - with open(fn) as instance_file: + with open(fn, encoding="utf-8") as instance_file: file_results.object_results = validate(instance_file, options) except Exception as ex: @@ -490,49 +497,100 @@ def validate_string(string, options=None): return validate(stream, options) -SCHEMA_STORE = {} +STIXValidator = extend(Draft202012Validator) -def ref_store(validator, ref, instance, schema): - """When validating '$ref' properties, add to global store. +# Built-in checker only ensures emails contain an '@'; we want a more robust check +@Draft202012Validator.FORMAT_CHECKER.checks('email') +def is_email(instance): + if not isinstance(instance, str): + return True + return EMAIL_RE.match(instance) + + +def from_uri_to_path(uri: str) -> str: + """Convert a URI to a file path if possible. + + Note: replace with 'Path.from_uri(schema_path_uri)' when Python 3.13. + + Args: + uri: the URI, potentially containing a file path. + + Returns: + The file path if the URI contains a file path, the URI otherwise. """ - remote_path = validator.resolver._urljoin_cache(validator.resolver.base_uri, ref) + parsed_url = parse.urlparse(uri) + if parsed_url.scheme == "file": + return request.url2pathname(parsed_url.path) + else: + return uri - if remote_path not in validator.resolver.store: - # Add local schema to Resolver store if present, so validator will use local - # schemas and only download remote refs in local is not present. - local_base_uri = validator.resolver._scopes_stack[0] - # Take out the the 'file:' prefix - if os.name == 'nt': - local_base_uri = local_base_uri[8:] - else: - local_base_uri = local_base_uri[5:] +def patch_schema(schema_data: dict, schema_path: str) -> dict: + """Patch schemas with two important fixes. - try: - local_filepath = os.path.abspath(os.path.join(local_base_uri, '../'+ref)) - local_schema = load_schema(local_filepath) - schema_id = local_schema.get('$id', '') - if schema_id: - validator.resolver.store[schema_id] = local_schema - except FileNotFoundError: - pass + 1) _SPECIFICATIONS dictionary inside referencing.jsonschema is a mapping + between URLs and validators; 'DRAFT201909' and 'DRAFT202012' are now + only recognized as such if the URLs is https://. + + 2) For relative references to work, the $id of the schema needs to point + to a valid URI or path, that can later be used as a key to retrieve + a loaded resource. + + Args: + schema_data: the schema itself. + schema_path: the path of the schema (can be both URI or path). - return Draft202012Validator.VALIDATORS['$ref'](validator, ref, instance, schema) + Returns: + The patched schema. + """ + schema_data = copy.copy(schema_data) + if "$schema" in schema_data: + schema_data["$schema"] = schema_data["$schema"].replace( + "http://json-schema.org/draft/", "https://json-schema.org/draft/" + ) + if "$id" in schema_data: + schema_path = pathlib.Path(schema_path) + if schema_path.is_absolute(): + schema_data["$id"] = str(schema_path.as_uri()) + else: + schema_data["$id"] = str(schema_path) + return schema_data -STIXValidator = extend(Draft202012Validator, {'$ref': ref_store}) +def retrieve_from_filesystem(schema_path_uri: str, schema_dir: str) -> Resource: + """Callback to retrieve a schema given its path. + Args: + schema_path_uri: the schema URI. + schema_dir: the optional directory of local schemas. -# Built-in checker only ensures emails contain an '@'; we want a more robust check -@Draft202012Validator.FORMAT_CHECKER.checks('email') -def is_email(instance): - if not isinstance(instance, str): - return True - return EMAIL_RE.match(instance) + Returns: + A resource loaded with the content. + """ + if schema_path_uri.startswith("http://") or schema_path_uri.startswith("https://"): + response = requests.get(schema_path_uri, allow_redirects=True) + schema = response.json() + schema = patch_schema(schema, schema_path_uri) + return Resource.from_contents(schema) + else: + schema_path = pathlib.Path(schema_path_uri) + if schema_path.is_absolute() or schema_path_uri.startswith("file://"): + is_relative = False + schema_path = from_uri_to_path(schema_path_uri) + else: + is_relative = True + schema_path = from_uri_to_path(os.path.join(schema_dir, schema_path_uri)) + with open(schema_path, "r") as f: + schema = json.load(f) + schema = patch_schema(schema, schema_path) + if is_relative: + return Resource.opaque(schema) + else: + return Resource.from_contents(schema) -def load_validator(schema_path, schema): +def load_validator(schema_path, schema, schema_dir): """Create a JSON schema validator for the given schema. Args: @@ -541,23 +599,13 @@ def load_validator(schema_path, schema): Returns: An instance of Draft202012Validator. - """ - global SCHEMA_STORE - - # Get correct prefix based on OS - if os.name == 'nt': - file_prefix = 'file:///' - else: - file_prefix = 'file:' - - resolver = RefResolver(file_prefix + schema_path.replace("\\", "/"), schema, store=SCHEMA_STORE) - schema_id = schema.get('$id', '') - if schema_id: - resolver.store[schema_id] = schema - # RefResolver creates a new store internally; persist it so we can use the same mappings every time - SCHEMA_STORE = resolver.store - validator = STIXValidator(schema, resolver=resolver, format_checker=Draft202012Validator.FORMAT_CHECKER) + schema = patch_schema(schema, schema_path) + retrieve_callback = functools.partial(retrieve_from_filesystem, schema_dir=schema_dir) + registry = Registry( + retrieve=retrieve_callback + ).with_resource(schema_path, DRAFT202012.create_resource(schema)) + validator = STIXValidator(schema, registry=registry, format_checker=Draft202012Validator.FORMAT_CHECKER) return validator @@ -661,7 +709,7 @@ def _get_error_generator(name, obj, schema_dir=None, version=DEFAULT_VER, defaul } # Don't use custom validator; only check schemas, no additional checks - validator = load_validator(schema_path, schema) + validator = load_validator(schema_path, schema, schema_dir) try: error_gen = validator.iter_errors(obj) except schema_exceptions.RefResolutionError: diff --git a/stix2validator/version.py b/stix2validator/version.py index 1fe90f6a..11731085 100644 --- a/stix2validator/version.py +++ b/stix2validator/version.py @@ -1 +1 @@ -__version__ = "3.1.4" +__version__ = "3.2.0" diff --git a/tox.ini b/tox.ini index 6f2a7f83..4c92584b 100644 --- a/tox.ini +++ b/tox.ini @@ -15,6 +15,8 @@ passenv = GITHUB_* [testenv:packaging] deps = twine + setuptools + wheel commands = python setup.py sdist bdist_wheel --universal twine check dist/*