diff --git a/.gitignore b/.gitignore index 9a9c7d31..a5ba1ac5 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ # pycharm .idea/ .idea +.pytest_cache/ # python setuptools build/ diff --git a/.travis.yml b/.travis.yml index e3485ff6..e99e66dd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,16 +1,19 @@ +os: linux language: python +cache: pip +dist: xenial python: - - "2.6" - "2.7" - - "3.3" - "3.4" - "3.5" - "3.6" + - "3.7" + - "3.8" install: + - pip install -U pip setuptools - pip install tox-travis script: - tox notifications: email: - - gback@mitre.org - - stix-commits-list@lists.mitre.org + - stix-commits-list@groups.mitre.org diff --git a/CHANGES.txt b/CHANGES.txt index db34abe5..b22812d1 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,41 @@ +Version 1.2.0.11 +2020-11-16 +- #367 Fix deprecation warning from collections module + +Version 1.2.0.10 +2020-05-01 +- #366 Check add_reference methods to prevent NoneType has no attribute 'append' +- Changes to STIXPackage to prevent the empty tag from appearing in serialization + +Version 1.2.0.9 +2020-04-16 +- #364 TTPs would fail to serialize XML Kill_Chains if no TTP was set +- Added Python 3.8 to test harness + +Version 1.2.0.8 +2020-03-09 +- #357 Add xnl:Type to the PersonName element (CIQ) +- Update the allowable values for PersonName and OrganisationName +- Update tests per recent CybOX release + +Version 1.2.0.7 +2019-09-06 +- Update package requirements + +Version 1.2.0.6 +2018-04-25 +- #347 Property targeted_technical_details missing in VictimTargeting class + +Version 1.2.0.5 +2018-03-06 +- #343 Create separate test environments for when maec is or is not installed +- #342 to_json fails on parsing datetime +- #339 [Python3 not supported - v1.1/1.2] to_json vaguely broken on some files +- #336 Report field ttp has the wrong type information +- #325 Bug in Indicator class setter for observables property +- Change stix.ToolInformation to use AttackerToolType vocab +- Removed Python 2.6 environment from CI tests + Version 1.2.0.4 2017-05-02 - Add support for Python 3.6. @@ -22,6 +60,65 @@ Version 1.2.0.2 - #302 Correctly handle CIQ Identity objects. - Add additional fields to CIQ Identity object. +Version 1.1.1.19 +2020-11-16 +- #367 Fix deprecation warning from collections module + +Version 1.1.1.18 +2020-05-01 +- #366 Check add_reference methods to prevent NoneType has no attribute 'append' +- Changes to STIXPackage to prevent the empty tag from appearing in serialization + +Version 1.1.1.17 +2020-04-21 +- #365 AISMarkingStructure not serializing correctly. PY3 compatibility issue + +Version 1.1.1.16 +2020-04-16 +- #364 TTPs would fail to serialize XML Kill_Chains if no TTP was set +- Added Python 3.8 to test harness + +Version 1.1.1.15 +2020-03-09 +- #357 Add xnl:Type to the PersonName element (CIQ) +- Update the allowable values for PersonName and OrganisationName +- Update tests per recent CybOX release + +Version 1.1.1.14 +2019-11-26 +- #361 fix problem with lookup_extension for TestMechanism + +Version 1.1.1.13 +2019-09-06 +- Update package requirements + +Version 1.1.1.12 +2018-04-25 +- #347 Property targeted_technical_details missing in VictimTargeting class + +Version 1.1.1.11 +2018-03-21 +- Fix issue with PyPI. + +Version 1.1.1.10 +2018-03-06 +- #343 Create separate test environments for when maec is or is not installed +- #339 [Python3 not supported - v1.1/1.2] to_json vaguely broken on some files +- #338 VocabTypes are unhashable in 1.1.1.x on Python 3 +- #325 Bug in Indicator class setter for observables property +- Change stix.ToolInformation to use AttackerToolType vocab +- Removed Python 2.6 environment from CI tests + +Version 1.1.1.9 +2017-05-25 +- Support Python 3.6 +- #321 Fix CIQ extensions. + +Version 1.1.1.8 +2017-01-18 +- Update to support Python 3. +- Convert to use mixbox. + Version 1.1.1.7 2016-10-21 - Improved handling of Industry Sectors in CIQ Identity for AIS Markings diff --git a/README.rst b/README.rst index fc01fbe7..e934477c 100644 --- a/README.rst +++ b/README.rst @@ -1,31 +1,31 @@ python-stix =========== -A python library for parsing, manipulating, and generating STIX v1.2 content. +A python library for parsing, manipulating, and generating `Structured Threat Information eXpression (STIX™) `_ v1.2.0 content. :Source: https://github.com/STIXProject/python-stix -:Documentation: http://stix.readthedocs.org +:Documentation: https://stix.readthedocs.io/ :Information: https://stixproject.github.io/ +:Download: https://pypi.python.org/pypi/stix/ -|travis badge| |landscape.io badge| |version badge| |downloads badge| +|travis_badge| |landscape_io_badge| |version_badge| -.. |travis badge| image:: https://api.travis-ci.org/STIXProject/python-stix.svg?branch=master +.. |travis_badge| image:: https://api.travis-ci.org/STIXProject/python-stix.svg?branch=master :target: https://travis-ci.org/STIXProject/python-stix :alt: Build Status -.. |landscape.io badge| image:: https://landscape.io/github/STIXProject/python-stix/master/landscape.svg +.. |landscape_io_badge| image:: https://landscape.io/github/STIXProject/python-stix/master/landscape.svg :target: https://landscape.io/github/STIXProject/python-stix/master :alt: Code Health -.. |version badge| image:: https://img.shields.io/pypi/v/stix.svg?maxAge=3600 - :target: https://pypi.python.org/pypi/stix/ -.. |downloads badge| image:: https://img.shields.io/pypi/dm/stix.svg?maxAge=3600 +.. |version_badge| image:: https://img.shields.io/pypi/v/stix.svg?maxAge=3600 :target: https://pypi.python.org/pypi/stix/ + :alt: Version Installation ------------ The python-stix library is hosted on `PyPI -`_ and the most recent stable version can be +`_ and the most recent stable version can be installed with `pip `_: :: @@ -61,13 +61,13 @@ Installation on Windows ~~~~~~~~~~~~~~~~~~~~~~~ Download the Lxml wheel for your version of Python from -http://www.lfd.uci.edu/~gohlke/pythonlibs/#lxml, then install it via "pip install +http://www.lfd.uci.edu/~gohlke/pythonlibs/#lxml, then install it via "pip install .whl". For example, to install it on 64-bit Windows running Python 2.7: :: - > pip install lxml-3.6.1-cp27-cp27m-win_amd64.whl - > pip install stix + $ pip install lxml-3.6.1-cp27-cp27m-win_amd64.whl + $ pip install stix Versioning ---------- diff --git a/docs/api/extensions/marking/ais.rst b/docs/api/extensions/marking/ais.rst new file mode 100644 index 00000000..79bd6973 --- /dev/null +++ b/docs/api/extensions/marking/ais.rst @@ -0,0 +1,297 @@ +:mod:`stix.extensions.marking.ais` Module +==================================================== + +.. automodule:: stix.extensions.marking.ais + +Classes +------- + +.. autoclass:: AISMarkingStructure + :show-inheritance: + :members: + +Functions +--------- + +.. autofunction:: add_ais_marking + + +Examples +-------- + +Applying AIS Markings +--------------------- + +The STIX specification allows data markings to be applied to any combination of +attributes and elements that can be described by XPath. That being said, the +Automated Indicator Sharing (AIS) capability requires those markings controlled +structure to select all nodes and attributes ``//node() | //@*``. All required +fields to create a valid AIS Markings are provided through the ``add_ais_marking`` +function. + +.. code-block:: python + + # python-stix imports + import stix + from stix.core import STIXPackage + from stix.extensions.marking.ais import (add_ais_marking, + COMMUNICATIONS_SECTOR, + INFORMATION_TECHNOLOGY_SECTOR) + from stix.indicator import Indicator + + # Create new STIX Package + stix_package = STIXPackage() + + # Create new Indicator + indicator = Indicator(title='My Indicator Example', + description='Example using AIS') + + # Add indicator to our STIX Package + stix_package.add_indicator(indicator) + + # Create AIS Marking with CIQ Identity and attach it to STIX Header. + add_ais_marking(stix_package, False, 'EVERYONE', 'GREEN', + country_name_code='US', + country_name_code_type='ISO 3166-1 alpha-2', + admin_area_name_code='US-VA', + admin_area_name_code_type='ISO 3166-2', + organisation_name='Example Corporation', + industry_type=[INFORMATION_TECHNOLOGY_SECTOR, COMMUNICATIONS_SECTOR] + ) + + # Print the XML. + print stix_package.to_xml() + + # Print the JSON. + print stix_package.to_json() + +This corresponds to the XML result: + +.. code-block:: xml + + + + + + //node() | //@* + + + + + + + + + \ + + + Example Corporation + + + + + + + + + + + + + + + + + + + + + + My Indicator Example + Example using AIS + + + + +The following corresponds to the JSON result: + +.. code-block:: json + + { + "stix_header": { + "handling": [ + { + "controlled_structure": "//node() | //@*", + "information_source": { + "identity": { + "xsi:type": "stix-ciqidentity:CIQIdentity3.0InstanceType", + "specification": { + "organisation_info": { + "industry_type": "Information Technology Sector|Communications Sector" + }, + "party_name": { + "organisation_names": [ + { + "name_elements": [ + { + "value": "Example Corporation" + } + ] + } + ] + }, + "addresses": [ + { + "country": { + "name_elements": [ + { + "name_code_type": "ISO 3166-1 alpha-2", + "name_code": "US" + } + ] + }, + "administrative_area": { + "name_elements": [ + { + "name_code_type": "ISO 3166-2", + "name_code": "US-VA" + } + ] + } + } + ] + } + } + }, + "marking_structures": [ + { + "xsi:type": "AIS:AISMarkingStructure", + "not_proprietary": { + "tlp_marking": { + "color": "GREEN" + }, + "ais_consent": { + "consent": "EVERYONE" + }, + "cisa_proprietary": "false" + } + } + ] + } + ] + }, + "version": "1.2", + "indicators": [ + { + "description": "Example using AIS", + "title": "My Indicator Example", + "timestamp": "2017-10-02T14:26:57.510000+00:00", + "id": "example:indicator-81466b8d-4efb-460f-ba13-b072420b9540" + } + ], + "id": "example:Package-a8c8135d-18d8-4384-903f-71285a02346e" + } + +Parsing AIS Markings +-------------------- + +Using the same example used for Applying AIS Markings. This would be how a +consumer of AIS would parse the data. + +.. code-block:: python + + # python-stix imports + import stix + from stix.core import STIXPackage + import stix.extensions.marking.ais # Register the AIS markings + + # Parse STIX Package + stix_package = STIXPackage.from_xml("stix_input.xml") + # stix_package = STIXPackage.from_json("stix_input.json") + + # Print all indicators + for indicator in stix_package.indicators: + print(indicator) + + # Extract markings from STIX Header + markings = stix_package.stix_header.handling + + # Print all markings contained in the STIX Header + for marking in markings: + print(marking) + print(marking.marking_structures) + print("----------MARKING CONTENT----------") + ais_struct = marking.marking_structures[0] + print("OBJ: %s" % ais_struct) + print("NotProprietary OBJ: %s" % ais_struct.not_proprietary) + print("CISA_Proprietary: %s" % ais_struct.not_proprietary.cisa_proprietary) + print("Consent: %s" % ais_struct.not_proprietary.ais_consent.consent) + print("TLP color: %s" % ais_struct.not_proprietary.tlp_marking.color) + + print("----------INFORMATION SOURCE----------") + identity = marking.information_source.identity.specification + print("OBJ: %s" % identity) + print("Organization Name: %s" % identity.party_name.organisation_names[0].name_elements[0].value) + print("Country: %s" % identity.addresses[0].country.name_elements[0].name_code) + print("Country code type: %s" % identity.addresses[0].country.name_elements[0].name_code_type) + print("Administrative area: %s" % identity.addresses[0].administrative_area.name_elements[0].name_code) + print("Administrative area code type: %s" % identity.addresses[0].administrative_area.name_elements[0].name_code_type) + print("Industry Type: %s" % identity.organisation_info.industry_type) + + + >>> + >>> + >>> [, ...] + >>> ----------MARKING CONTENT---------- + >>> OBJ: + >>> NotProprietary OBJ: + >>> CISA_Proprietary: False + >>> Consent: EVERYONE + >>> TLP color: GREEN + >>> ----------INFORMATION SOURCE---------- + >>> OBJ: + >>> Organization Name: Example Corporation + >>> Country: US + >>> Country code type: ISO 3166-1 alpha-2 + >>> Administrative area: US-VA + >>> Administrative area code type: ISO 3166-2 + >>> Industry Type: Information Technology Sector|Communications Sector + +Constants +--------- + +The following constants can be used for the ``industry_type`` keyword argument to +``add_ais_marking``: + +.. autodata:: CHEMICAL_SECTOR +.. autodata:: COMMERCIAL_FACILITIES_SECTOR +.. autodata:: COMMUNICATIONS_SECTOR +.. autodata:: CRITICAL_MANUFACTURING_SECTOR +.. autodata:: DAMS_SECTOR +.. autodata:: DEFENSE_INDUSTRIAL_BASE_SECTOR +.. autodata:: EMERGENCY_SERVICES_SECTOR +.. autodata:: ENERGY_SECTOR +.. autodata:: FINANCIAL_SERVICES_SECTOR +.. autodata:: FOOD_AND_AGRICULTURE_SECTOR +.. autodata:: GOVERNMENT_FACILITIES_SECTOR +.. autodata:: HEALTH_CARE_AND_PUBLIC_HEALTH_SECTOR +.. autodata:: INFORMATION_TECHNOLOGY_SECTOR +.. autodata:: NUCLEAR_REACTORS_MATERIALS_AND_WASTE_SECTOR +.. autodata:: OTHER +.. autodata:: TRANSPORTATION_SYSTEMS_SECTOR +.. autodata:: WATER_AND_WASTEWATER_SYSTEMS_SECTOR diff --git a/docs/api/indicator/indicator.rst b/docs/api/indicator/indicator.rst index 0b299213..ccc78ff9 100644 --- a/docs/api/indicator/indicator.rst +++ b/docs/api/indicator/indicator.rst @@ -29,7 +29,7 @@ Classes add_short_description, add_test_mechanism, add_valid_time_position, alternative_id, confidence, description, descriptions, find, get_produced_time, get_received_time, handling, id_, idref, - indicated_ttps, indicator_types, information_source, kill_chain_phases, + indicated_ttps, indicator_types, kill_chain_phases, likely_impact, negate, observable, observable_composition_operator, observables, producer, related_campaigns, related_indicators, related_packages, set_produced_time, set_producer_identity, diff --git a/docs/getting_started.rst b/docs/getting_started.rst index 10e291ba..5a48b125 100644 --- a/docs/getting_started.rst +++ b/docs/getting_started.rst @@ -10,7 +10,7 @@ This page gives an introduction to **python-stix** and how to use it. Prerequisites ------------- -The python-stix library provides an API for creating or processing STIX content. As such, it is a developer tool that can be leveraged by those who know Python 2.6/2.7 and are familiar with object-oriented programming practices, Python package layouts, and are comfortable with the installation of Python libraries. To contribute code to the python-stix repository, users must be familiar with `git`_ and `GitHub pull request`_ methodologies. Understanding XML, XML Schema, and the STIX language is also incredibly helpful when using python-stix in an application. +The python-stix library provides an API for creating or processing STIX content. As such, it is a developer tool that can be leveraged by those who know Python 2.7/3.3+ and are familiar with object-oriented programming practices, Python package layouts, and are comfortable with the installation of Python libraries. To contribute code to the python-stix repository, users must be familiar with `git`_ and `GitHub pull request`_ methodologies. Understanding XML, XML Schema, and the STIX language is also incredibly helpful when using python-stix in an application. .. _git: http://git-scm.com/documentation .. _GitHub pull request: https://help.github.com/articles/using-pull-requests diff --git a/docs/index.rst b/docs/index.rst index b26e81c1..a650a9fb 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -22,17 +22,17 @@ version of STIX. ============ =================== STIX Version python-stix Version ============ =================== -1.2 1.2.0.4 (`PyPI`__) (`GitHub`__) -1.1.1 1.1.1.8 (`PyPI`__) (`GitHub`__) -1.1.0 1.1.0.6 (`PyPI`__) (`GitHub`__) -1.0.1 1.0.1.1 (`PyPI`__) (`GitHub`__) -1.0 1.0.0a7 (`PyPI`__) (`GitHub`__) +1.2 1.2.0.11 (`PyPI`__) (`GitHub`__) +1.1.1 1.1.1.18 (`PyPI`__) (`GitHub`__) +1.1.0 1.1.0.6 (`PyPI`__) (`GitHub`__) +1.0.1 1.0.1.1 (`PyPI`__) (`GitHub`__) +1.0 1.0.0a7 (`PyPI`__) (`GitHub`__) ============ =================== -__ https://pypi.python.org/pypi/stix/1.2.0.4 -__ https://github.com/STIXProject/python-stix/tree/v1.2.0.4 -__ https://pypi.python.org/pypi/stix/1.1.1.8 -__ https://github.com/STIXProject/python-stix/tree/v1.1.1.8 +__ https://pypi.python.org/pypi/stix/1.2.0.11 +__ https://github.com/STIXProject/python-stix/tree/v1.2.0.11 +__ https://pypi.python.org/pypi/stix/1.1.1.18 +__ https://github.com/STIXProject/python-stix/tree/v1.1.1.18 __ https://pypi.python.org/pypi/stix/1.1.0.6 __ https://github.com/STIXProject/python-stix/tree/v1.1.0.6 __ https://pypi.python.org/pypi/stix/1.0.1.1 @@ -71,6 +71,18 @@ API Reference api/index api/coverage +FAQ +=== + +- My RAM consumption rises when processing a large amount of files. + This problem is caused by a python-cybox_ caching mechanism that is enabled + by default. To prevent this issue from happening use the + ``cybox.utils.caches.cache_clear()`` method in your code/script to release + the cached resources as appropriate. Refer to the ``cybox`` documentation + for more details. + +.. _python-cybox: http://cybox.readthedocs.io/ + Contributing ============ If a bug is found, a feature is missing, or something just isn't behaving the way you'd expect it to, please submit an issue to our `tracker`_. If you'd like to contribute code to our repository, you can do so by issuing a `pull request`_ and we will work with you to try and integrate that code into our repository. diff --git a/examples/vuln_affected_software.py b/examples/vuln_affected_software.py index 3f1d5532..620087c0 100644 --- a/examples/vuln_affected_software.py +++ b/examples/vuln_affected_software.py @@ -28,7 +28,7 @@ # Wrap the Product Object in an Observable instance observable = Observable(software) -# Attach the Product observable to the affected_sofware list of +# Attach the Product observable to the affected_software list of # RelatedObservable instances. This wraps our Observable in a # RelatedObservable layer. vuln = Vulnerability() diff --git a/requirements.txt b/requirements.txt index c27803bd..14df8061 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -maec>=4.1.0.13.dev4,<4.1.1.0 # For tests that include MAEC +maec>=4.1.0.13,<4.1.1.0 # For tests that include MAEC nose==1.3.7 sphinx==1.3.6 sphinx_rtd_theme==0.2.4 diff --git a/setup.cfg b/setup.cfg index 2be68365..e383efad 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,17 @@ +[bumpversion] +current_version = 1.2.0.11 +parse = (?P\d+)\.(?P\d+)\.(?P\d+).(?P\d+) +serialize = {major}.{minor}.{patch}.{revision} +commit = True +tag = True + +[bumpversion:file:stix/version.py] + +[bumpversion:file:docs/index.rst] + +[metadata] +license_file = LICENSE.txt + [bdist_wheel] universal = True + diff --git a/setup.py b/setup.py index 74f729dd..717d437d 100644 --- a/setup.py +++ b/setup.py @@ -3,6 +3,7 @@ # Copyright (c) 2017, The MITRE Corporation. All rights reserved. # See LICENSE.txt for complete terms. +from io import open # Allow `encoding` kwarg on Python 2.7 from os.path import abspath, dirname, join @@ -11,6 +12,7 @@ BASE_DIR = dirname(abspath(__file__)) VERSION_FILE = join(BASE_DIR, 'stix', 'version.py') + def get_version(): with open(VERSION_FILE) as f: for line in f.readlines(): @@ -20,15 +22,17 @@ def get_version(): raise AttributeError("Package does not have a __version__") -with open('README.rst') as f: - readme = f.read() +def get_long_description(): + with open('README.rst', encoding='utf-8') as f: + return f.read() install_requires = [ - 'lxml>=2.3', + 'lxml>=2.2.3 ; python_version == "2.7" or python_version >= "3.5"', + 'lxml>=2.2.3,<4.4.0 ; python_version > "2.7" and python_version < "3.5"', + 'mixbox>=1.0.4', + 'cybox>=2.1.0.13,<2.1.1.0', 'python-dateutil', - 'cybox>=2.1.0.13.dev1,<2.1.1.0', - 'mixbox>=1.0.2', ] @@ -38,22 +42,28 @@ def get_version(): author="STIX Project, MITRE Corporation", author_email="stix@mitre.org", description="An API for parsing and generating STIX content.", - long_description=readme, - url="http://stix.mitre.org", + long_description=get_long_description(), + url="https://stixproject.github.io/", packages=find_packages(), install_requires=install_requires, + license="BSD", classifiers=[ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', - ] + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + ], + project_urls={ + 'Documentation': 'https://stix.readthedocs.io/', + 'Source Code': 'https://github.com/STIXProject/python-stix/', + 'Bug Tracker': 'https://github.com/STIXProject/python-stix/issues/', + }, ) diff --git a/stix/base.py b/stix/base.py index 14f50a6d..b5c417ee 100644 --- a/stix/base.py +++ b/stix/base.py @@ -8,6 +8,7 @@ from sys import version_info # mixbox +from mixbox import compat from mixbox import idgen from mixbox import entities from mixbox import fields @@ -311,7 +312,7 @@ def istypeof(cls, obj): return isinstance(obj, cls) -class TypedList(TypedCollection, collections.MutableSequence): +class TypedList(TypedCollection, compat.MutableSequence): def __init__(self, *args): TypedCollection.__init__(self, *args) diff --git a/stix/bindings/extensions/malware/maec_4_1.py b/stix/bindings/extensions/malware/maec_4_1.py index e32a0ad1..0e0d5a5d 100644 --- a/stix/bindings/extensions/malware/maec_4_1.py +++ b/stix/bindings/extensions/malware/maec_4_1.py @@ -15,12 +15,6 @@ from stix.bindings import register_extension import stix.bindings.ttp as ttp_binding -try: - from maec.bindings.maec_package import PackageType - maec_installed = True -except ImportError: - PackageType = None - maec_installed = False XML_NS = "http://stix.mitre.org/extensions/Malware#MAEC4.1-1" @@ -28,10 +22,11 @@ # Data representation classes. # + @register_extension class MAEC4_1InstanceType(ttp_binding.MalwareInstanceType): """The MAEC4.1InstanceType provides an extension to ttp_binding.MalwareInstanceType - which imports and leverages the MAEC 4.0.1 schema for structured + which imports and leverages the MAEC 4.1 schema for structured characterization of Malware.""" subclass = None superclass = ttp_binding.MalwareInstanceType @@ -89,11 +84,7 @@ def exportChildren(self, lwrite, level, nsmap, namespace_=XML_NS, name_='MAEC4.1 else: eol_ = '' if self.MAEC is not None: - if maec_installed and isinstance(self.MAEC, PackageType): - self.MAEC.export(lwrite, level, namespace_='stix-maec:', name_='MAEC', pretty_print=pretty_print) - else: - showIndent(lwrite, level, pretty_print) - lwrite(etree_.tostring(self.MAEC, pretty_print=pretty_print).decode()) + self.MAEC.export(lwrite, level, namespace_='stix-maec:', name_='MAEC', pretty_print=pretty_print) def build(self, node): self.__sourcenode__ = node already_processed = set() @@ -105,12 +96,11 @@ def buildAttributes(self, node, attrs, already_processed): super(MAEC4_1InstanceType, self).buildAttributes(node, attrs, already_processed) def buildChildren(self, child_, node, nodeName_, fromsubclass_=False): if nodeName_ == 'MAEC': - if maec_installed: - obj_ = PackageType.factory() - obj_.build(child_) - self.set_MAEC(obj_) - else: - self.set_MAEC(child_) + # Fails hard if maec library is not installed in your Python environment. + from maec.bindings.maec_package import PackageType + obj_ = PackageType.factory() + obj_.build(child_) + self.set_MAEC(obj_) super(MAEC4_1InstanceType, self).buildChildren(child_, node, nodeName_, True) # end class MAEC4_1InstanceType diff --git a/stix/bindings/stix_common.py b/stix/bindings/stix_common.py index 64cfac7c..8ae31be4 100644 --- a/stix/bindings/stix_common.py +++ b/stix/bindings/stix_common.py @@ -3495,7 +3495,7 @@ class StructuredTextType(GeneratedsSuper): Note that if the markup tags used by this format would be interpreted as XML information (such as the bracket-based tags of HTML) the text area should be enclosed in a CDATA section to prevent the markup from - interferring with XMLvalidation of the CybOX document. If this + interfering with XMLvalidation of the CybOX document. If this attribute is absent, the implication is that no markup is being used.""" subclass = None superclass = None diff --git a/stix/common/information_source.py b/stix/common/information_source.py index f7042f27..ef718ba7 100644 --- a/stix/common/information_source.py +++ b/stix/common/information_source.py @@ -22,7 +22,7 @@ class InformationSource(stix.Entity): _binding_class = stix_common_binding.InformationSourceType _namespace = 'http://stix.mitre.org/common-1' - identity = fields.TypedField("Identity", type_=Identity, factory=IdentityFactory) + identity = fields.TypedField("Identity", Identity, factory=IdentityFactory) descriptions = fields.TypedField("Description", StructuredTextList) contributing_sources = fields.TypedField("Contributing_Sources", type_="stix.common.information_source.ContributingSources") time = fields.TypedField("Time", cybox.common.Time) @@ -39,15 +39,15 @@ def __init__(self, description=None, identity=None, time=None, tools=None, contr self.time = time self.tools = tools self.references = references - #self.roles = None def add_contributing_source(self, value): self.contributing_sources.append(value) - def add_reference(self, value): if not value: return + if self.references is None: + self.references = References() # TODO: Check if it's a valid URI? self.references.append(value) @@ -80,7 +80,6 @@ def add_description(self, description): """ self.descriptions.add(description) - def add_role(self, value): self.roles.append(value) @@ -95,4 +94,3 @@ class ContributingSources(stix.EntityList): @classmethod def _dict_as_list(cls): return False - \ No newline at end of file diff --git a/stix/common/profiles.py b/stix/common/profiles.py index a4499d01..1e523b6c 100644 --- a/stix/common/profiles.py +++ b/stix/common/profiles.py @@ -3,13 +3,14 @@ import collections +from mixbox import compat from mixbox import fields import stix from stix.bindings import stix_common as stix_common_binding -class Profiles(collections.MutableSequence, stix.Entity): +class Profiles(compat.MutableSequence, stix.Entity): _binding = stix_common_binding _binding_class = stix_common_binding.ProfilesType _namespace = 'http://stix.mitre.org/common-1' diff --git a/stix/common/references.py b/stix/common/references.py index 45bfbb27..f5c7dc1e 100644 --- a/stix/common/references.py +++ b/stix/common/references.py @@ -2,13 +2,14 @@ # See LICENSE.txt for complete terms. import collections +from mixbox import compat from mixbox import fields import stix from stix.bindings import stix_common as stix_common_binding -class References(collections.MutableSequence, stix.Entity): +class References(compat.MutableSequence, stix.Entity): _binding = stix_common_binding _binding_class = stix_common_binding.ReferencesType _namespace = 'http://stix.mitre.org/common-1' diff --git a/stix/common/structured_text.py b/stix/common/structured_text.py index b90c0396..9dc73b47 100644 --- a/stix/common/structured_text.py +++ b/stix/common/structured_text.py @@ -10,7 +10,13 @@ import stix import stix.utils as utils import stix.bindings.stix_common as stix_common_binding -from mixbox.vendor.six import text_type +from mixbox.vendor.six import PY2, PY3, text_type + + +if PY2: + from collections import Sequence +elif PY3: + from collections.abc import Sequence #: Default ordinality value for StructuredText. @@ -58,7 +64,7 @@ def to_dict(self): """Converts this object into a dictionary representation. Note: - If no properies or attributes are set other than ``value``, + If no properties or attributes are set other than ``value``, this will return a string. """ @@ -89,7 +95,7 @@ def _unset_default(text): """Unsets the ordinality of the StructuredText object `text` if the ordinality is equal to the DEFAULT_ORDINALITY. - The ordinaity will be returned to its original state after exiting the + The ordinality will be returned to its original state after exiting the context manager. """ @@ -105,7 +111,7 @@ def _unset_default(text): text.ordinality = ordinality -class StructuredTextList(stix.TypedCollection, collections.Sequence): +class StructuredTextList(stix.TypedCollection, Sequence): """A sequence type used to store StructureText objects. Args: diff --git a/stix/common/tools.py b/stix/common/tools.py index 6d32d266..5cc0f155 100644 --- a/stix/common/tools.py +++ b/stix/common/tools.py @@ -3,7 +3,9 @@ # external from mixbox import fields + import cybox.common +from cybox.common.vocabs import VocabField # internal import stix @@ -11,6 +13,7 @@ # relative from .structured_text import StructuredTextList +from .vocabs import AttackerToolType class ToolInformation(stix.Entity, cybox.common.ToolInformation): @@ -20,6 +23,7 @@ class ToolInformation(stix.Entity, cybox.common.ToolInformation): title = fields.TypedField("Title") short_descriptions = fields.TypedField("Short_Description", StructuredTextList) + type_ = VocabField("Type", AttackerToolType, multiple=True) def __init__(self, title=None, short_description=None, tool_name=None, tool_vendor=None): super(ToolInformation, self).__init__(tool_name=tool_name, tool_vendor=tool_vendor) diff --git a/stix/common/vocabs.py b/stix/common/vocabs.py index 98b74a58..462d757b 100644 --- a/stix/common/vocabs.py +++ b/stix/common/vocabs.py @@ -5,9 +5,10 @@ from functools import partial # mixbox -from mixbox import fields -from mixbox import entities -from mixbox import typedlist +from mixbox import entities, fields, typedlist + +# cybox +from cybox.common import vocabs # stix import stix @@ -24,7 +25,7 @@ def validate_value(instance, value): elif value in allowed: return else: - error = "Value must be one of {allowed}. Received '{value}'" + error = "Value for vocab {instance.__class__} must be one of {allowed}. Received '{value}'" error = error.format(**locals()) raise ValueError(error) @@ -124,7 +125,6 @@ def to_dict(self): return self.value return super(VocabString, self).to_dict() - @classmethod def from_dict(cls, cls_dict): if not cls_dict: @@ -306,8 +306,9 @@ class DiscoveryMethod_2_0(VocabString): TERM_USER = "User" +@vocabs.register_vocab @register_vocab -class AttackerToolType_1_0(VocabString): +class AttackerToolType_1_0(vocabs.VocabString): _namespace = 'http://stix.mitre.org/default_vocabularies-1' _XSI_TYPE = 'stixVocabs:AttackerToolTypeVocab-1.0' _VOCAB_VERSION = '1.0' diff --git a/stix/core/stix_package.py b/stix/core/stix_package.py index 14e9fbb1..da2d5643 100644 --- a/stix/core/stix_package.py +++ b/stix/core/stix_package.py @@ -104,7 +104,7 @@ def __init__(self, id_=None, idref=None, timestamp=None, stix_header=None, self.indicators = indicators or Indicators() self.incidents = incidents or Incidents() self.threat_actors = threat_actors or ThreatActors() - self.ttps = ttps or TTPs() + self.ttps = ttps self.related_packages = related_packages self.reports = reports or Reports() self.timestamp = timestamp @@ -180,7 +180,7 @@ def add_ttp(self, ttp): """ if self.ttps is None: self.ttps = TTPs() - self.ttps.append(ttp) + self.ttps.ttp.append(ttp) def add_report(self, report): """Adds a :class:`.Report` object to the :attr:`reports` collection. diff --git a/stix/core/ttps.py b/stix/core/ttps.py index d5620509..9f1cb265 100644 --- a/stix/core/ttps.py +++ b/stix/core/ttps.py @@ -5,12 +5,10 @@ from functools import partial # mixbox -from mixbox import entities from mixbox import fields # stix import stix -from stix import utils from stix.ttp import TTP from stix.common.kill_chains import KillChains from stix.bindings import stix_core as core_binding @@ -19,24 +17,23 @@ from stix.utils.deprecated import IdrefDeprecatedList -class TTPs(stix.EntityList): +class TTPs(stix.Entity): _binding = core_binding _binding_class = _binding.TTPsType _namespace = 'http://stix.mitre.org/stix-1' - ttps = fields.TypedField( - name="TTP", - type_=TTP, - multiple=True, - key_name="ttps", - listfunc=partial(IdrefDeprecatedList, type=TTP) - ) - + ttp = fields.TypedField("TTP", TTP, multiple=True, key_name="ttps", listfunc=partial(IdrefDeprecatedList, type=TTP)) kill_chains = fields.TypedField("Kill_Chains", KillChains) def __init__(self, ttps=None): - super(TTPs, self).__init__(ttps) + super(TTPs, self).__init__() + self.ttp = ttps self.kill_chains = KillChains() def add_ttp(self, ttp): - self.append(ttp) + self.ttp.append(ttp) + + def add_kill_chain(self, kc): + if self.kill_chains is None: + self.kill_chains = KillChains() + self.kill_chains.kill_chain.append(kc) diff --git a/stix/exploit_target/vulnerability.py b/stix/exploit_target/vulnerability.py index 78e0974a..814c2f19 100644 --- a/stix/exploit_target/vulnerability.py +++ b/stix/exploit_target/vulnerability.py @@ -4,13 +4,10 @@ from mixbox import fields import stix -import stix.utils as utils import stix.bindings.exploit_target as exploit_target_binding -import stix.bindings.stix_common as stix_common_binding -from stix.common import DateTimeWithPrecision, StructuredTextList +from stix.common import DateTimeWithPrecision, References, StructuredTextList from stix.common.related import GenericRelationshipList, RelatedObservable -from mixbox import entities, fields -from stix.common import References + class Vulnerability(stix.Entity): """Implementation of STIX ``Vulnerability``. @@ -44,7 +41,6 @@ def __init__(self, title=None, description=None, short_description=None): self.title = title self.descriptions = StructuredTextList(description) self.short_descriptions = StructuredTextList(short_description) - self.references = [] @property def description(self): @@ -102,9 +98,11 @@ def add_short_description(self, description): def add_reference(self, reference): if not reference: return - + if self.references is None: + self.references = References() self.references.append(reference) + class CVSSVector(stix.Entity): """ Common Vulnerabilit Scoring System object, representing its component measures diff --git a/stix/extensions/identity/ciq_identity_3_0.py b/stix/extensions/identity/ciq_identity_3_0.py index a2d89f08..2010ac8f 100644 --- a/stix/extensions/identity/ciq_identity_3_0.py +++ b/stix/extensions/identity/ciq_identity_3_0.py @@ -1028,13 +1028,53 @@ class PersonName(stix.Entity): _namespace = XML_NS_XNL XML_TAG = "{%s}PersonName" % _namespace - def __init__(self, name_elements=None): + TYPE_ALIAS = 'Alias' + TYPE_LEGAL_NAME = 'LegalName' + TYPE_KNOWN_AS = 'KnownAs' + TYPE_MAIDEN_NAME = 'MaidenName' + TYPE_FORMER_NAME = 'FormerName' + TYPE_COMMON_USE = 'CommonUse' + TYPE_NAME_AT_BIRTH = 'NameAtBirth' + TYPE_PREFERRED_NAME = 'PreferredName' + TYPE_OFFICIAL_NAME = 'OfficialName' + TYPE_UNOFFICIAL_NAME = 'UnofficialName' + TYPE_NICK_NAME = 'NickName' + TYPE_PET_NAME = 'PetName' + + TYPES = ( + TYPE_ALIAS, + TYPE_LEGAL_NAME, + TYPE_KNOWN_AS, + TYPE_MAIDEN_NAME, + TYPE_FORMER_NAME, + TYPE_COMMON_USE, + TYPE_NAME_AT_BIRTH, + TYPE_PREFERRED_NAME, + TYPE_OFFICIAL_NAME, + TYPE_UNOFFICIAL_NAME, + TYPE_NICK_NAME, + TYPE_PET_NAME, + ) + + def __init__(self, name_elements=None, type_=None): self.name_elements = [] + self.type_ = type_ if name_elements: for name_element in name_elements: self.add_name_element(name_element) + @property + def type_(self): + return self._type + + @type_.setter + def type_(self, value): + if value and value not in self.TYPES: + raise ValueError('value must be one of %s: ' % (self.TYPES,)) + + self._type = value + def add_name_element(self, value): if isinstance(value, string_types): self.name_elements.append(PersonNameElement(value=value)) @@ -1050,6 +1090,9 @@ def to_obj(self, return_obj=None, ns_info=None): root_tag = PersonName.XML_TAG return_obj = et.Element(root_tag) + if self.type_: + return_obj.attrib['{%s}Type' % XML_NS_XNL] = self.type_ + for name_element in self.name_elements: return_obj.append(name_element.to_obj(ns_info=ns_info)) @@ -1063,6 +1106,8 @@ def from_obj(cls, obj, return_obj=None): if not return_obj: return_obj = cls() + return_obj.type_ = obj.attrib.get('{%s}Type' % XML_NS_XNL) + name_elements = obj.findall(PersonNameElement.XML_TAG) if name_elements: for name_element_obj in name_elements: @@ -1074,6 +1119,9 @@ def from_obj(cls, obj, return_obj=None): def to_dict(self): d = {} + if self.type_: + d['type'] = self.type_ + if self.name_elements: for ne in self.name_elements: d.setdefault('name_elements', []).append(ne.to_dict()) @@ -1088,6 +1136,8 @@ def from_dict(cls, dict_repr, return_obj=None): if not return_obj: return_obj = cls() + return_obj.type_ = dict_repr.get('type') + ne_dicts = dict_repr.get('name_elements', []) for ne_dict in ne_dicts: @@ -1100,11 +1150,40 @@ class OrganisationName(stix.Entity): _namespace = XML_NS_XNL XML_TAG = "{%s}OrganisationName" % _namespace + TYPE_LEGAL_NAME = 'LegalName' + TYPE_FORMER_NAME = 'FormerName' + TYPE_COMMON_USE = 'CommonUse' + TYPE_PUBLISHING_NAME = 'PublishingName' + TYPE_OFFICIAL_NAME = 'OfficialName' + TYPE_UNOFFICIAL_NAME = 'UnofficialName' + TYPE_UNDEFINED = 'Undefined' + + TYPES = ( + TYPE_LEGAL_NAME, + TYPE_FORMER_NAME, + TYPE_COMMON_USE, + TYPE_PUBLISHING_NAME, + TYPE_OFFICIAL_NAME, + TYPE_UNOFFICIAL_NAME, + TYPE_UNDEFINED, + ) + def __init__(self, name_elements=None, subdivision_names=None, type_=None): self.type_ = type_ self.name_elements = name_elements self.subdivision_names = subdivision_names + @property + def type_(self): + return self._type + + @type_.setter + def type_(self, value): + if value and value not in self.TYPES: + raise ValueError('value must be one of %s: ' % (self.TYPES,)) + + self._type = value + @property def name_elements(self): return self._name_elements @@ -1235,9 +1314,6 @@ def value(self): @value.setter def value(self, value): - # if not value: - # raise ValueError('value cannot be None') - self._value = value @classmethod @@ -1270,8 +1346,8 @@ class PersonNameElement(_BaseNameElement): _namespace = XML_NS_XNL XML_TAG = "{%s}NameElement" % _namespace - TYPE_TITLE = 'Title' TYPE_PRECEDING_TITLE = 'PrecedingTitle' + TYPE_TITLE = 'Title' TYPE_FIRST_NAME = 'FirstName' TYPE_MIDDLE_NAME = 'MiddleName' TYPE_LAST_NAME = 'LastName' diff --git a/stix/extensions/malware/maec_4_1_malware.py b/stix/extensions/malware/maec_4_1_malware.py index b1f873c8..57e0751a 100644 --- a/stix/extensions/malware/maec_4_1_malware.py +++ b/stix/extensions/malware/maec_4_1_malware.py @@ -1,231 +1,37 @@ # Copyright (c) 2017, The MITRE Corporation. All rights reserved. # See LICENSE.txt for complete terms. -# stdlib -from distutils.version import LooseVersion - # external -from lxml import etree -import mixbox.xml -from mixbox.vendor.six import BytesIO, iteritems, binary_type +from mixbox import fields + # internal import stix -import stix.utils as utils -import stix.ttp.malware_instance +from stix.bindings.extensions.malware import maec_4_1 as maec_instance_binding from stix.ttp.malware_instance import MalwareInstance -import stix.bindings.extensions.malware.maec_4_1 as ext_binding -from mixbox import fields -from stix.bindings.extensions.malware.maec_4_1 import maec_installed -from lxml.etree import _ElementTree - -_MIN_PYTHON_MAEC_VERSION = '4.1.0.12' - - -class UnsupportedVersion(Exception): - def __init__(self, message, expected, found): - super(UnsupportedVersion, self).__init__(message) - self.expected = expected - self.found = found -def _check_maec_version(): - """Checks that the installed python-maec has a version greater than or - equal to the minimum supported version. - - Note: - We do this rather than having a python-maec dependency requirement - listed in setup.py because MAEC is used as an extension to STIX and - not a core component to STIX (like CybOX). - - Raises: - ImportError: If python-maec is not installed. - UnsupportedVersion: If the python-maec installation does not satisfy - the version requirements. - +@stix.register_extension +class MAECInstance(MalwareInstance): """ - import maec - - found = maec.__version__ - expected = _MIN_PYTHON_MAEC_VERSION - - if LooseVersion(found) >= LooseVersion(expected): - return + The MAECInstance object provides an extension to the MalwareInstanceType + which imports and leverages the MAEC 4.1 schema for structured + characterization of Malware. - fmt = ("Unsupported python-maec version installed: '%s'. Minimum version " - "is '%s'.") - error = fmt % (found, expected) - raise UnsupportedVersion(error, expected=expected, found=found) - - -try: - # Check that the correct version of python-maec is installed. - _check_maec_version() - - # Import maecPackage into global space - from maec.package.package import Package as maecPackage - - _MAEC_INSTALLED = True -except ImportError: - maecPackage, Package = None, None - _MAEC_INSTALLED = False - - -def is_maec(obj): - """Checks if the input object is python-maec object. - - Returns: - True if python-maec is ins + This class extension is automatically registered by the + MalwareInstanceFactory. + Warnings: + Interacting with the ``maec`` field will fail if the maec library is + not installed in your Python environment. """ - if not _MAEC_INSTALLED: - return False - - return isinstance(obj, maecPackage) - -def validate_maec_input(instance, value): - if value is None: - return - elif _MAEC_INSTALLED and is_maec(value): - return - elif mixbox.xml.is_element(value) or mixbox.xml.is_etree(value): - return - else: - error = ( - "Cannot set maec to '{0}'. Expected 'lxml.etree._Element' or " - "'maec.package.package.Package'." - ) - error = error.format(type(value)) - raise ValueError(error) - -@stix.register_extension -class MAECInstance(MalwareInstance): - _binding = ext_binding + _binding = maec_instance_binding _binding_class = _binding.MAEC4_1InstanceType - _namespace = 'http://stix.mitre.org/extensions/Malware#MAEC4.1-1' - _xml_ns_prefix = "stix-maec" + _namespace = "http://stix.mitre.org/extensions/Malware#MAEC4.1-1" _XSI_TYPE = "stix-maec:MAEC4.1InstanceType" - _TAG_MAEC = "{%s}MAEC" % _namespace - maec = fields.TypedField("MAEC", preset_hook=validate_maec_input) + maec = fields.TypedField("MAEC", type_="maec.package.package.Package") def __init__(self, maec=None): super(MAECInstance, self).__init__() - self.__input_namespaces__ = {} - self.__input_schemalocations__ = {} self.maec = maec - - def _parse_etree(self, root): - node_tag = root.tag - - if node_tag != self._TAG_MAEC: - self._cast_maec(root) - - self._collect_namespaces(root) - self._collect_schemalocs(root) - - def _cast_maec(self, node): - ns_maec = "http://maec.mitre.org/XMLSchema/maec-package-2" - node_ns = etree.QName(node).namespace - - if node_ns == ns_maec: - etree.register_namespace(self._xml_ns_prefix, self._namespace) - node.tag = self._TAG_MAEC - else: - error = "Cannot set maec. Expected tag '{0}' found '{1}'." - error = error.format(self._TAG_MAEC, node.tag) - raise ValueError(error) - - def _collect_schemalocs(self, node): - try: - schemaloc = mixbox.xml.get_schemaloc_pairs(node) - self.__input_schemalocations__ = dict(schemaloc) - except KeyError: - self.__input_schemalocations__ = {} - - def _collect_namespaces(self, node): - self.__input_namespaces__ = dict(iteritems(node.nsmap)) - - @classmethod - def from_obj(cls, obj): - if not obj: - return None - - return_obj = cls() - - if _MAEC_INSTALLED: - obj.MAEC = maecPackage.from_obj(obj.MAEC) - else: - obj.MAEC = obj.MAEC - - return_obj = super(MAECInstance, cls).from_obj(obj) - - return return_obj - - def to_obj(self, ns_info=None): - return_obj = super(MAECInstance, self).to_obj(ns_info=ns_info) - - if mixbox.xml.is_element(self.maec) or mixbox.xml.is_etree(self.maec): - tree = mixbox.xml.get_etree(self.maec) - root = mixbox.xml.get_etree_root(tree) - self._parse_etree(root) - self.maec = root - - if _MAEC_INSTALLED and isinstance(self.maec, maecPackage): - return_obj.MAEC = self.maec.to_obj(ns_info=ns_info) - else: - return_obj.MAEC = self.maec - - return return_obj - - @classmethod - def _maec_from_dict(cls, d): - if _MAEC_INSTALLED: - return maecPackage.from_dict(d) - - raise ValueError( - "Unable to parse 'maec' value in dictionary. Please " - "install python-maec to parse dictionary value." - ) - - @classmethod - def from_dict(cls, d, return_obj=None): - if not d: - return None - - d = d.copy() - - maec = d.get('maec') - - if maec is None: - pass - elif isinstance(maec, dict): - d['maec'] = cls._maec_from_dict(maec) - elif isinstance(maec, binary_type): - d['maec'] = mixbox.xml.get_etree_root(BytesIO(maec)) - else: - raise TypeError("Unknown type for 'maec' entry.") - - return_obj = super(MAECInstance, cls).from_dict(d) - - return return_obj - - def to_dict(self): - d = super(MAECInstance, self).to_dict() - - if self.maec is not None: - if mixbox.xml.is_element(self.maec) or mixbox.xml.is_etree(self.maec): - tree = mixbox.xml.get_etree(self.maec) - root = mixbox.xml.get_etree_root(tree) - self._parse_etree(root) - self.maec = root - - if _MAEC_INSTALLED and isinstance(self.maec, maecPackage): - d['maec'] = self.maec.to_dict() - else: - d['maec'] = etree.tostring(self.maec) - - if self._XSI_TYPE: - d['xsi:type'] = self._XSI_TYPE - - return d diff --git a/stix/extensions/marking/ais.py b/stix/extensions/marking/ais.py index bd2b76cf..5686ca30 100644 --- a/stix/extensions/marking/ais.py +++ b/stix/extensions/marking/ais.py @@ -1,6 +1,23 @@ # Copyright (c) 2017, The MITRE Corporation. All rights reserved. # See LICENSE.txt for complete terms. +""" +STIX Extension for AIS Data Markings + +Unlike the other marking extensions, the AIS marking extension is not loaded +automatically, since AIS markings are not a part of the published STIX 1.x +specifications. They are included in python-stix because they're common enough +that it is not worth creating a separate package. + +If you are writing code that needs to parse AIS markings, make sure that your +program imports this module before beginning to parse any STIX documents: + +.. code-block:: python + + import stix.extensions.marking.ais + +""" + from mixbox import fields from mixbox.namespaces import Namespace @@ -120,22 +137,39 @@ def _update_namespaces(): # IndustryType allowed sectors +#: Chemical Sector CHEMICAL_SECTOR = 'Chemical Sector' +#: Chemical Sector COMMERCIAL_FACILITIES_SECTOR = 'Commercial Facilities Sector' +#: Commercial Facilities Sector COMMUNICATIONS_SECTOR = 'Communications Sector' +#: Critical Manufacturing Sector CRITICAL_MANUFACTURING_SECTOR = 'Critical Manufacturing Sector' +#: Dams Sector DAMS_SECTOR = 'Dams Sector' +#: Defense Industrial Base Sector DEFENSE_INDUSTRIAL_BASE_SECTOR = 'Defense Industrial Base Sector' +#: Emergency Services Sector EMERGENCY_SERVICES_SECTOR = 'Emergency Services Sector' +#: Energy Sector ENERGY_SECTOR = 'Energy Sector' +#: Financial Services Sector FINANCIAL_SERVICES_SECTOR = 'Financial Services Sector' +#: Food and Agriculture Sector FOOD_AND_AGRICULTURE_SECTOR = 'Food and Agriculture Sector' +#: Government Facilities Sector GOVERNMENT_FACILITIES_SECTOR = 'Government Facilities Sector' +#: Healthcare and Public Health Sector HEALTH_CARE_AND_PUBLIC_HEALTH_SECTOR = 'Healthcare and Public Health Sector' +#: Information Technology Sector INFORMATION_TECHNOLOGY_SECTOR = 'Information Technology Sector' +#: Nuclear Reactors, Materials, and Waste Sector NUCLEAR_REACTORS_MATERIALS_AND_WASTE_SECTOR = 'Nuclear Reactors, Materials, and Waste Sector' +#: Other OTHER = 'Other' +#: Transportation Systems Sector TRANSPORTATION_SYSTEMS_SECTOR = 'Transportation Systems Sector' +#: Water and Wastewater Systems Sector WATER_AND_WASTEWATER_SYSTEMS_SECTOR = 'Water and Wastewater Systems Sector' @@ -192,10 +226,11 @@ def add_ais_marking(stix_package, proprietary, consent, color, **kwargs): """ This utility functions aids in the creation of an AIS marking and appends it to the provided STIX package. + Args: stix_package: A stix.core.STIXPackage object. proprietary: True if marking uses IsProprietary, False for - NotProprietary. + NotProprietary. consent: A string with one of the following values: "EVERYONE", "NONE" or "USG". color: A string that corresponds to TLP values: "WHITE", "GREEN" or @@ -204,19 +239,26 @@ def add_ais_marking(stix_package, proprietary, consent, color, **kwargs): identity object. These are: country_name_code, country_name_code_type, admin_area_name_code, admin_area_name_code_type, organisation_name, industry_type. + Raises: ValueError: When keyword arguments are missing. User did not supply correct values for: proprietary, color and consent. + Note: - The following line is required to register the AIS extension: - >>> import stix.extensions.marking.ais + The following line is required to register the AIS extension:: + + >>> import stix.extensions.marking.ais + Any Markings under STIX Header will be removed. Please follow the guidelines for `AIS`_. + The industry_type keyword argument accepts: a list of string based on defined sectors, a pipe-delimited string of sectors, or a single sector. + .. _AIS: https://www.us-cert.gov/ais + """ from stix.common import InformationSource from stix.extensions.identity.ciq_identity_3_0 import ( diff --git a/stix/incident/history.py b/stix/incident/history.py index 5b44aa1a..175d2ed0 100644 --- a/stix/incident/history.py +++ b/stix/incident/history.py @@ -1,16 +1,16 @@ # Copyright (c) 2017, The MITRE Corporation. All rights reserved. # See LICENSE.txt for complete terms. +from mixbox import fields + # internal import stix -import stix.utils as utils import stix.bindings.incident as incident_binding from stix.common.datetimewithprecision import DATETIME_PRECISION_VALUES # relative from .coa import COATaken -from mixbox import fields, entities def validate_precision(instance, value): if value and (value not in DATETIME_PRECISION_VALUES): @@ -18,6 +18,7 @@ def validate_precision(instance, value): error = error.format(DATETIME_PRECISION_VALUES, value) raise ValueError(error) + class JournalEntry(stix.Entity): _namespace = "http://stix.mitre.org/Incident-1" _binding = incident_binding @@ -25,7 +26,7 @@ class JournalEntry(stix.Entity): value = fields.TypedField("valueOf_", key_name="value") author = fields.TypedField("author") - time = fields.TypedField("time") #TDOO: utils.dates.parse_value(value) + time = fields.DateTimeField("time") time_precision = fields.TypedField("time_precision", preset_hook=validate_precision) def __init__(self, value=None): @@ -38,10 +39,10 @@ class HistoryItem(stix.Entity): _namespace = "http://stix.mitre.org/Incident-1" _binding = incident_binding _binding_class = incident_binding.HistoryItemType - + action_entry = fields.TypedField("Action_Entry", COATaken) journal_entry = fields.TypedField("Journal_Entry", JournalEntry) - + def __init__(self): super(HistoryItem, self).__init__() @@ -52,8 +53,7 @@ class History(stix.EntityList): _binding_class = incident_binding.HistoryType history_items = fields.TypedField("History_Item", HistoryItem, multiple=True, key_name="history_items") - + @classmethod def _dict_as_list(cls): return False - \ No newline at end of file diff --git a/stix/indicator/indicator.py b/stix/indicator/indicator.py index a9549717..fc941045 100644 --- a/stix/indicator/indicator.py +++ b/stix/indicator/indicator.py @@ -281,7 +281,7 @@ def observables(self, value): elif utils.is_sequence(value): if len(value) == 1: - self.observable = value + self.add_observable(value[0]) return observable_comp = ObservableComposition() @@ -470,7 +470,7 @@ def add_related_indicator(self, indicator): ``related_indicators`` list property. Calling this method is the same as calling ``append()`` on the - ``related_indicators`` proeprty. + ``related_indicators`` property. See Also: The :class:`RelatedIndicators` documentation. diff --git a/stix/report/__init__.py b/stix/report/__init__.py index bf994581..b83070e7 100644 --- a/stix/report/__init__.py +++ b/stix/report/__init__.py @@ -43,7 +43,7 @@ class Report(stix.Entity): ``datetime.datetime`` or ``str``. header: A Report :class:`.Header` object. campaigns: A collection of :class:`.Campaign` objects. - course_of_action: A collection of :class:`.CourseOfAction` objects. + courses_of_action: A collection of :class:`.CourseOfAction` objects. exploit_targets: A collection of :class:`.ExploitTarget` objects. incidents: A collection of :class:`.Incident` objects. indicators: A collection of :class:`.Indicator` objects. @@ -59,7 +59,7 @@ class Report(stix.Entity): id_ = fields.IdField("id") idref = fields.IdrefField("idref") - timestamp = fields.TypedField("timestamp") + timestamp = fields.DateTimeField("timestamp") version = fields.TypedField("version") header = fields.TypedField("Header", Header) campaigns = fields.TypedField("Campaigns", type_="stix.report.Campaigns") @@ -69,7 +69,7 @@ class Report(stix.Entity): indicators = fields.TypedField("Indicators", type_="stix.report.Indicators") incidents = fields.TypedField("Incidents", type_="stix.report.Incidents") threat_actors = fields.TypedField("Threat_Actors", type_="stix.report.ThreatActors") - ttps = fields.TypedField("TTPs", type_="stix.core.ttps.TTPs") + ttps = fields.TypedField("TTPs", type_="stix.report.TTPs") related_reports = fields.TypedField("Related_Reports", type_="stix.report.RelatedReports") def __init__(self, id_=None, idref=None, timestamp=None, header=None, @@ -207,6 +207,7 @@ def add(self, entity): error = error.format(type(entity)) raise TypeError(error) + class Campaigns(stix.EntityList): _binding = report_binding _namespace = 'http://stix.mitre.org/Report-1' @@ -243,7 +244,6 @@ class Indicators(stix.EntityList): _binding = report_binding _namespace = 'http://stix.mitre.org/Report-1' _binding_class = _binding.IndicatorsType - _contained_type = Indicator indicator = fields.TypedField("Indicator", Indicator, multiple=True, key_name="indicators") @@ -255,4 +255,10 @@ class ThreatActors(stix.EntityList): threat_actor = fields.TypedField("Threat_Actor", ThreatActor, multiple=True, key_name="threat_actors") -from stix.core.ttps import TTPs + +class TTPs(stix.EntityList): + _binding = report_binding + _namespace = 'http://stix.mitre.org/Report-1' + _binding_class = _binding.TTPsType + + ttp = fields.TypedField("TTP", TTP, multiple=True, key_name="ttps") diff --git a/stix/test/coa_test.py b/stix/test/coa_test.py index 9a3c0e17..05d2b905 100644 --- a/stix/test/coa_test.py +++ b/stix/test/coa_test.py @@ -51,9 +51,9 @@ class COATests(EntityTestCase, unittest.TestCase): }, 'objective': ObjectiveTests._full_dict, 'parameter_observables': { - 'major_version': 2, - 'minor_version': 1, - 'update_version': 0, + 'cybox_major_version': '2', + 'cybox_minor_version': '1', + 'cybox_update_version': '0', 'observables': [ { 'idref': "example:Observable-1" diff --git a/stix/test/core/stix_package_test.py b/stix/test/core/stix_package_test.py index bfde2d15..0ce35e13 100644 --- a/stix/test/core/stix_package_test.py +++ b/stix/test/core/stix_package_test.py @@ -12,7 +12,7 @@ from . import stix_header_test -from stix import core +from stix import core, report from stix.core import stix_package from stix.campaign import Campaign from stix.coa import CourseOfAction @@ -39,6 +39,7 @@ class COAsTests(EntityTestCase, unittest.TestCase): {'idref': 'example:test-1'} ] + class ExploitTargetsTests(EntityTestCase, unittest.TestCase): klass = stix_package.ExploitTargets @@ -46,6 +47,7 @@ class ExploitTargetsTests(EntityTestCase, unittest.TestCase): {'idref': 'example:test-1'} ] + class IncidentsTests(EntityTestCase, unittest.TestCase): klass = stix_package.Incidents @@ -53,6 +55,7 @@ class IncidentsTests(EntityTestCase, unittest.TestCase): {'idref': 'example:test-1'} ] + class IndicatorsTests(EntityTestCase, unittest.TestCase): klass = stix_package.Indicators @@ -68,6 +71,7 @@ class ThreatActorsTests(EntityTestCase, unittest.TestCase): {'idref': 'example:test-1'} ] + class TTPsTests(EntityTestCase, unittest.TestCase): klass = stix_package.TTPs @@ -97,9 +101,9 @@ class STIXPackageTests(EntityTestCase, unittest.TestCase): 'incidents': IncidentsTests._full_dict, 'indicators': IndicatorsTests._full_dict, 'observables': { - 'major_version': 2, - 'minor_version': 1, - 'update_version': 0, + 'cybox_major_version': '2', + 'cybox_minor_version': '1', + 'cybox_update_version': '0', 'observables': [ { 'idref': "example:Observable-1" @@ -188,6 +192,9 @@ def test_related_package_idref_deprecation(self): package = core.STIXPackage() package.add_related_package(core.STIXPackage(idref='foo')) + def test_setting_report_ttps_fails_on_stix_package(self): + self.assertRaises(TypeError, core.STIXPackage(), report.TTPs(), 'TTPs must be a , not a ') + if __name__ == "__main__": unittest.main() diff --git a/stix/test/extensions/identity/ciq_identity_3_0_test.py b/stix/test/extensions/identity/ciq_identity_3_0_test.py index 20d7d101..542371e9 100644 --- a/stix/test/extensions/identity/ciq_identity_3_0_test.py +++ b/stix/test/extensions/identity/ciq_identity_3_0_test.py @@ -40,8 +40,16 @@ class CIQIdentity3_0InstanceTests(EntityTestCase, unittest.TestCase): ], 'person_names': [ { + 'type': 'LegalName', 'name_elements': [ - {'value': 'John Smith'} + { + 'element_type': 'FirstName', + 'value': 'John', + }, + { + 'element_type': 'LastName', + 'value': 'Smith', + } ] }, { diff --git a/stix/test/extensions/malware/maec_4_1_malware_test.py b/stix/test/extensions/malware/maec_4_1_malware_test.py index d8e9084b..30257c3c 100644 --- a/stix/test/extensions/malware/maec_4_1_malware_test.py +++ b/stix/test/extensions/malware/maec_4_1_malware_test.py @@ -1,14 +1,18 @@ import unittest from stix.core import STIXPackage -from mixbox.vendor.six import StringIO, BytesIO, text_type - -from lxml import etree -import mixbox.xml +from mixbox.vendor.six import StringIO, text_type from stix.test import EntityTestCase from stix.extensions.malware.maec_4_1_malware import MAECInstance +try: + import maec + maec_present = True +except ImportError: + maec_present = False + +@unittest.skipIf(condition=maec_present is False, reason="These tests require the 'maec' library.") class PythonMAECTests(EntityTestCase, unittest.TestCase): klass = MAECInstance @@ -17,18 +21,20 @@ class PythonMAECTests(EntityTestCase, unittest.TestCase): 'maec': { 'malware_subjects': [ - {'malware_instance_object_attributes': - {'id': 'maec-tst-obj-1', - 'properties': { - 'hashes': - [ - { - 'simple_hash_value': '9d7006e30fdf15e9c8e03e62534b3a3e', - 'type': 'MD5' - } - ], - 'xsi:type': 'FileObjectType'} - } + { + 'malware_instance_object_attributes': { + 'id': 'maec-tst-obj-1', + 'properties': { + 'hashes': + [ + { + 'simple_hash_value': '9d7006e30fdf15e9c8e03e62534b3a3e', + 'type': 'MD5' + } + ], + 'xsi:type': 'FileObjectType' + } + } } ] } @@ -43,81 +49,11 @@ def test_add_name_type(self): self.assertTrue("Remote Access Trojan" in maec_xml) -class PythonMAECEtreeTests(unittest.TestCase): - XML = ( - """ - - - - - - - - MD5 - 9d7006e30fdf15e9c8e03e62534b3a3e - - - - - - - - """ - ) - - def _test_xml(self, obj): - xml = obj.to_xml() - parser = mixbox.xml.get_xml_parser() - tree = etree.parse(BytesIO(xml), parser=parser) - root = tree.getroot() - - xpath = "//cyboxCommon:Type" - nodes = root.xpath(xpath, namespaces={'cyboxCommon': 'http://cybox.mitre.org/common-2'}) - - self.assertTrue(nodes is not None) - self.assertEqual(len(nodes), 1) - self.assertEqual(nodes[0].text, "MD5") - - - def test_etree(self): - parser = mixbox.xml.get_xml_parser() - tree = etree.parse(StringIO(self.XML), parser=parser) - - ext = MAECInstance() - ext.maec = tree - self._test_xml(ext) - - - def test_etree_dict(self): - parser = mixbox.xml.get_xml_parser() - tree = etree.parse(StringIO(self.XML), parser=parser) - ext = MAECInstance() - ext.maec = tree - - d = ext.to_dict() - ext2 = MAECInstance.from_dict(d) - self._test_xml(ext2) - - +@unittest.skipIf(condition=maec_present is False, reason="These tests require the 'maec' library.") class PythonMAECInPackageTests(unittest.TestCase): XML = StringIO( """ - @@ -150,7 +86,7 @@ class PythonMAECInPackageTests(unittest.TestCase): ) XML_MAEC = StringIO( """ - @@ -198,16 +134,39 @@ def test_parse_malware(self): """Test parsing a normal MalwareInstance from XML """ stix_pkg = STIXPackage.from_xml(self.XML) - mw = stix_pkg.ttps[0].behavior.malware_instances[0].to_dict() + mw = stix_pkg.ttps.ttp[0].behavior.malware_instances[0].to_dict() self.assertTrue('names' in mw) def test_parse_malware_maec(self): """Test parsing a MaecInstance from XML """ stix_pkg = STIXPackage.from_xml(self.XML_MAEC) - mw = stix_pkg.ttps[0].behavior.malware_instances[0].to_dict() + mw = stix_pkg.ttps.ttp[0].behavior.malware_instances[0].to_dict() self.assertTrue('names' in mw) +@unittest.skipIf(condition=maec_present is True, reason="These tests require the 'maec' library to be missing.") +class PythonMAECNotInstalledTest(unittest.TestCase): + + def test_parsing_maec_fails(self): + try: + STIXPackage.from_xml(PythonMAECInPackageTests.XML_MAEC) + except ImportError as e: + self.assertTrue(all(x in str(e) for x in ("No module named", "maec"))) + + def test_handling_maec_object_fails(self): + try: + MAECInstance().from_dict(PythonMAECTests._full_dict) + except ImportError as e: + self.assertTrue(all(x in str(e) for x in ("No module named", "maec"))) + + def test_setting_maec_property_fails(self): + try: + m = MAECInstance() + m.maec = "foo" + except ImportError as e: + self.assertTrue(all(x in str(e) for x in ("No module named", "maec"))) + + if __name__ == "__main__": unittest.main() diff --git a/stix/test/indicator_test.py b/stix/test/indicator_test.py index a8f06c68..953a076c 100644 --- a/stix/test/indicator_test.py +++ b/stix/test/indicator_test.py @@ -6,6 +6,7 @@ from cybox.core import Observable, ObservableComposition from cybox.objects.file_object import File +import mixbox.typedlist from mixbox.vendor.six import text_type from stix.core import STIXPackage @@ -493,6 +494,36 @@ def test_observables_property_standard(self): self.assertEqual([obs.to_dict()], [x.to_dict() for x in ind2.observables]) + def test_set_indicator_observables_to_single_observable(self): + # https://github.com/STIXProject/python-stix/issues/325 + i = Indicator() + o1 = Observable() + o2 = Observable() + + i.observables = o1 + self.assertEqual(type([]), type(i.observables)) + self.assertEqual(1, len(i.observables)) + + def test_set_indicator_observables_to_list_of_two_observables(self): + # https://github.com/STIXProject/python-stix/issues/325 + i = Indicator() + o1 = Observable() + o2 = Observable() + + i.observables = [o1, o2] + self.assertEqual(mixbox.typedlist.TypedList, type(i.observables)) + self.assertEqual(2, len(i.observables)) + + def test_set_indicator_observables_to_list_of_one_observable(self): + # https://github.com/STIXProject/python-stix/issues/325 + i = Indicator() + o1 = Observable() + o2 = Observable() + + i.observables = [o1] + self.assertEqual(type([]), type(i.observables)) + self.assertEqual(1, len(i.observables)) + class RelatedCampaignReferencesTests(unittest.TestCase, EntityTestCase): klass = RelatedCampaignRefs diff --git a/stix/test/report_test.py b/stix/test/report_test.py index 264276db..2ce43055 100644 --- a/stix/test/report_test.py +++ b/stix/test/report_test.py @@ -4,10 +4,11 @@ import unittest from stix import report +from stix.core.ttps import TTPs from stix.test import EntityTestCase, data_marking_test -from stix.test.common import (kill_chains_test, information_source_test, - structured_text_test, related_test) +from stix.test.common import (information_source_test, structured_text_test, + related_test) class HeaderTests(EntityTestCase, unittest.TestCase): @@ -50,6 +51,7 @@ class COAsTests(EntityTestCase, unittest.TestCase): {'idref': 'example:test-1'} ] + class ExploitTargetsTests(EntityTestCase, unittest.TestCase): klass = report.ExploitTargets @@ -57,6 +59,7 @@ class ExploitTargetsTests(EntityTestCase, unittest.TestCase): {'idref': 'example:test-1'} ] + class IncidentsTests(EntityTestCase, unittest.TestCase): klass = report.Incidents @@ -64,6 +67,7 @@ class IncidentsTests(EntityTestCase, unittest.TestCase): {'idref': 'example:test-1'} ] + class IndicatorsTests(EntityTestCase, unittest.TestCase): klass = report.Indicators @@ -83,12 +87,9 @@ class ThreatActorsTests(EntityTestCase, unittest.TestCase): class TTPsTests(EntityTestCase, unittest.TestCase): klass = report.TTPs - _full_dict = { - 'kill_chains': kill_chains_test.KillChainsTests._full_dict, - 'ttps': [ - {'idref': 'example:test-1'} - ] - } + _full_dict = [ + {'idref': 'example:test-1'} + ] class ReportTests(EntityTestCase, unittest.TestCase): @@ -101,9 +102,9 @@ class ReportTests(EntityTestCase, unittest.TestCase): 'incidents': IncidentsTests._full_dict, 'indicators': IndicatorsTests._full_dict, 'observables': { - 'major_version': 2, - 'minor_version': 1, - 'update_version': 0, + 'cybox_major_version': '2', + 'cybox_minor_version': '1', + 'cybox_update_version': '0', 'observables': [ { 'idref': "example:Observable-1" @@ -116,6 +117,9 @@ class ReportTests(EntityTestCase, unittest.TestCase): 'version': "1.0" } + def test_report_with_stix_core_ttps_fails(self): + self.assertRaises(TypeError, self.klass().ttps, TTPs(), 'TTPs must be a , not a ') + if __name__ == "__main__": unittest.main() diff --git a/stix/test/ttp_test.py b/stix/test/ttp_test.py index 8e6b537a..c7c4a339 100644 --- a/stix/test/ttp_test.py +++ b/stix/test/ttp_test.py @@ -12,7 +12,7 @@ import stix.ttp as ttp from stix.ttp import ( resource, infrastructure, exploit_targets, malware_instance, exploit, - attack_pattern, behavior + attack_pattern, behavior, victim_targeting ) @@ -52,9 +52,9 @@ class InfrastructureTests(EntityTestCase, unittest.TestCase): 'short_description': 'Short Description', 'types': ['foo', 'bar'], 'observable_characterization': { - 'major_version': 2, - 'minor_version': 1, - 'update_version': 0, + 'cybox_major_version': '2', + 'cybox_minor_version': '1', + 'cybox_update_version': '0', 'observables': [ { 'idref': "example:Observable-1" @@ -71,7 +71,13 @@ class ResourcesTests(EntityTestCase, unittest.TestCase): 'personas': PersonasTests._full_dict, 'tools': [ { - 'title': "Tool" + 'title': "Tool", + 'type': [ + { + 'value': 'Malware', + 'xsi:type': 'stixVocabs:AttackerToolTypeVocab-1.0' + } + ] } ], 'infrastructure': InfrastructureTests._full_dict @@ -81,7 +87,7 @@ class ResourcesTests(EntityTestCase, unittest.TestCase): class MalwareInstanceTests(EntityTestCase, unittest.TestCase): klass = malware_instance.MalwareInstance - _full_dict = _full_dict = { + _full_dict = { 'id': 'example:test-1', 'title': 'Title', 'description': 'Description', @@ -156,6 +162,44 @@ class BehaviorTests(EntityTestCase, unittest.TestCase): 'attack_patterns': AttackPatternsTests._full_dict } + +class VictimTargetingTests(EntityTestCase, unittest.TestCase): + klass = victim_targeting.VictimTargeting + + _full_dict = { + 'identity': { + 'specification': { + 'organisation_info': { + 'industry_type': 'Electricity, Industrial Control Systems' + } + }, + 'xsi:type': 'stix-ciqidentity:CIQIdentity3.0InstanceType' + }, + 'targeted_systems': [ + { + 'value': 'Industrial Control Systems', + 'xsi:type': 'stixVocabs:SystemTypeVocab-1.0' + } + ], + 'targeted_information': [ + { + 'value': 'Information Assets - Intellectual Property', + 'xsi:type': 'stixVocabs:InformationTypeVocab-1.0' + } + ], + 'targeted_technical_details': { + 'cybox_major_version': '2', + 'cybox_minor_version': '1', + 'cybox_update_version': '0', + 'observables': [ + { + 'idref': "example:Observable-2" + } + ] + } + } + + class TTPTests(EntityTestCase, unittest.TestCase): klass = ttp.TTP _full_dict = { @@ -169,7 +213,8 @@ class TTPTests(EntityTestCase, unittest.TestCase): 'exploit_targets': ExploitTargetsTests._full_dict, 'behavior': BehaviorTests._full_dict, 'related_packages': related_test.RelatedPackageRefsTests._full_dict, - 'kill_chain_phases': kill_chains_test.KillChainPhasesReferenceTests._full_dict + 'kill_chain_phases': kill_chains_test.KillChainPhasesReferenceTests._full_dict, + 'victim_targeting': VictimTargetingTests._full_dict } def test_add_description(self): diff --git a/stix/ttp/malware_instance.py b/stix/ttp/malware_instance.py index 0d3b6ca7..d80359bd 100644 --- a/stix/ttp/malware_instance.py +++ b/stix/ttp/malware_instance.py @@ -3,7 +3,6 @@ # internal import stix -import stix.utils as utils from stix.common import vocabs from stix.common import StructuredTextList, VocabString @@ -17,7 +16,7 @@ class MalwareInstance(stix.Entity): _binding = ttp_binding _binding_class = _binding.MalwareInstanceType _namespace = "http://stix.mitre.org/TTP-1" - _XSI_TYPE = None # defined by subclasses + _XSI_TYPE = None # defined by subclasses id_ = fields.IdField("id") idref = fields.IdrefField("idref") @@ -105,11 +104,21 @@ def lookup_class(xsi_type): return stix.lookup_extension(xsi_type) + def to_dict(self): + d = super(MalwareInstance, self).to_dict() + + if self._XSI_TYPE: + d["xsi:type"] = self._XSI_TYPE + + return d + class MalwareInstanceFactory(entities.EntityFactory): @classmethod def entity_class(cls, key): + from stix.extensions.malware.maec_4_1_malware import MAECInstance # noqa return stix.lookup_extension(key, default=MalwareInstance) + # Backwards compatibility add_extension = stix.add_extension diff --git a/stix/ttp/resource.py b/stix/ttp/resource.py index e6b99d9b..cabdef4a 100644 --- a/stix/ttp/resource.py +++ b/stix/ttp/resource.py @@ -2,19 +2,15 @@ # See LICENSE.txt for complete terms. # mixbox -from mixbox import fields -from mixbox import typedlist +from mixbox import fields, typedlist # internal import stix +import stix.bindings.ttp as ttp_binding from stix.common import ToolInformation from stix.common.identity import Identity, IdentityFactory -import stix.bindings.ttp as ttp_binding - -# relative -from .infrastructure import Infrastructure +from stix.ttp.infrastructure import Infrastructure -from mixbox import entities, fields class _IdentityList(typedlist.TypedList): def __init__(self, *args): diff --git a/stix/ttp/victim_targeting.py b/stix/ttp/victim_targeting.py index 8649c1b9..f6364193 100644 --- a/stix/ttp/victim_targeting.py +++ b/stix/ttp/victim_targeting.py @@ -7,7 +7,7 @@ # internal import stix import stix.bindings.ttp as ttp_binding -from stix.common import vocabs, VocabString +from stix.common import vocabs from stix.common.identity import Identity, IdentityFactory from mixbox import fields @@ -21,6 +21,7 @@ class VictimTargeting(stix.Entity): targeted_systems = vocabs.VocabField("Targeted_Systems", vocabs.SystemType, multiple=True) targeted_information = vocabs.VocabField("Targeted_Information", vocabs.InformationType, multiple=True) + targeted_technical_details = fields.TypedField("Targeted_Technical_Details", Observables) def __init__(self): super(VictimTargeting, self).__init__() diff --git a/stix/version.py b/stix/version.py index 956c4394..a2a3a195 100644 --- a/stix/version.py +++ b/stix/version.py @@ -1,4 +1,4 @@ # Copyright (c) 2017, The MITRE Corporation. All rights reserved. # See LICENSE.txt for complete terms. -__version__ = "1.2.0.5.dev0" +__version__ = "1.2.0.11" diff --git a/tox.ini b/tox.ini index 450e4418..275e8bdf 100644 --- a/tox.ini +++ b/tox.ini @@ -1,19 +1,18 @@ [tox] -envlist = py26, py27, py33, py34, py35, py36, lxml23 +envlist = py27, py34, py35, py36, py37, py38, lxml23, docs, no-maec, packaging [testenv] commands = nosetests stix # NOTE: python-stix does not have any doctests # sphinx-build -b doctest docs docs/_build/doctest - sphinx-build -b html docs docs/_build/html deps = -rrequirements.txt # We call this "lxml23" instead of "rhel6", since RHEL6 ships with LXML 2.2.3. # python-stix requires at least 2.3. [testenv:lxml23] -basepython=python2.6 +basepython=python2.7 commands = nosetests stix deps = @@ -22,11 +21,29 @@ deps = python-dateutil==1.4.1 -rrequirements.txt +# Test the behavior when MAEC is not installed in the environment. +[testenv:no-maec] +commands = + nosetests stix +deps = + nose==1.3.7 + tox==2.7.0 + +[testenv:docs] +commands = + sphinx-build -W -b html -d {envtmpdir}/doctrees docs {envtmpdir}/html + +[testenv:packaging] +deps = + readme_renderer +commands = + python setup.py check -r -s + [travis] python = - 2.6: py26, lxml23 - 2.7: py27, docs - 3.3: py33 - 3.4: py34 - 3.5: py35 - 3.6: py36 + 2.7: py27, docs, lxml23, no-maec, packaging + 3.4: py34, no-maec + 3.5: py35, no-maec + 3.6: py36, no-maec, packaging + 3.7: py37, no-maec + 3.8: py38, no-maec