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