diff --git a/README.md b/README.md deleted file mode 100644 index aad8c5e..0000000 --- a/README.md +++ /dev/null @@ -1,248 +0,0 @@ -[![Build Status](https://travis-ci.org/oasis-open/cti-python-stix2.svg?branch=master)](https://travis-ci.org/oasis-open/cti-python-stix2) -[![codecov](https://codecov.io/gh/oasis-open/cti-python-stix2/branch/master/graph/badge.svg)](https://codecov.io/gh/oasis-open/cti-python-stix2) - -# cti-python-stix2 - -*This is an [OASIS Open Repository](https://www.oasis-open.org/resources/open-repositories/). See the [Governance](#governance) section for more information.* - -This repository provides Python APIs for serializing and de-serializing STIX 2 JSON content, along with higher-level APIs for common tasks, including data markings, versioning, and for resolving STIX IDs across multiple data sources. - -For more information, see [the documentation](https://stix2.readthedocs.io/en/latest/) on ReadTheDocs. - -## Installation - -Install with [`pip`](https://pip.pypa.io/en/stable/): - -``` -pip install stix2 -``` - -## Usage - -### Creating STIX Domain Objects - -To create a STIX object, provide keyword arguments to the type's constructor: - -```python -from stix2 import Indicator - -indicator = Indicator(name="File hash for malware variant", - labels=['malicious-activity'], - pattern='file:hashes.md5 = "d41d8cd98f00b204e9800998ecf8427e"') - -``` - -Certain required attributes of all objects will be set automatically if not -provided as keyword arguments: - -- If not provided, `type` will be set automatically to the correct type. - You can also provide the type explicitly, but this is not necessary: - - ```python - indicator = Indicator(type='indicator', ...) - ``` - - Passing a value for `type` that does not match the class being constructed - will cause an error: - - ```python - >>> indicator = Indicator(type='xxx', ...) - stix2.exceptions.InvalidValueError: Invalid value for Indicator 'type': must equal 'indicator'. - ``` - -- If not provided, `id` will be generated randomly. If you provide an `id` - argument, it must begin with the correct prefix: - - ```python - >>> indicator = Indicator(id="campaign--63ce9068-b5ab-47fa-a2cf-a602ea01f21a") - stix2.exceptions.InvalidValueError: Invalid value for Indicator 'id': must start with 'indicator--'. - ``` - -- If not provided, `created` and `modified` will be set to the (same) current - time. - -For indicators, `labels` and `pattern` are required and cannot be set -automatically. Trying to create an indicator that is missing one of these -properties will result in an error: - -```python ->>> indicator = Indicator() -stix2.exceptions.MissingPropertiesError: No values for required properties for Indicator: (labels, pattern). -``` - -However, the required `valid_from` attribute on Indicators will be set to the -current time if not provided as a keyword argument. - -Once created, the object acts like a frozen dictionary. Properties can be -accessed using the standard Python dictionary syntax: - -```python ->>> indicator['name'] -'File hash for malware variant' -``` - -TBD: Should we allow property access using the standard Python attribute syntax? - -```python ->>> indicator.name -'File hash for malware variant' -``` - -Attempting to modify any attributes will raise an error: - -```python ->>> indicator['name'] = "This is a revised name" -TypeError: 'Indicator' object does not support item assignment ->>> indicator.name = "This is a revised name" -stix2.exceptions.ImmutableError: Cannot modify properties after creation. -``` - -To update the properties of an object, see [Versioning](#versioning) below. - -Creating a Malware object follows the same pattern: - -```python -from stix2 import Malware - -malware = Malware(name="Poison Ivy", - labels=['remote-access-trojan']) -``` - -As with indicators, the `type`, `id`, `created`, and `modified` properties will -be set automatically if not provided. For Malware objects, the `labels` and -`name` properties must be provided. - -### Creating Relationships - -STIX 2 Relationships are separate objects, not properties of the object on -either side of the relationship. They are constructed similarly to other STIX -objects. The `type`, `id`, `created`, and `modified` properties are added -automatically if not provided. Callers must provide the `relationship_type`, -`source_ref`, and `target_ref` properties. - -```python -from stix2 import Relationship - -relationship = Relationship(relationship_type='indicates', - source_ref=indicator.id, - target_ref=malware.id) -``` - -The `source_ref` and `target_ref` properties can be either the ID's of other -STIX objects, or the STIX objects themselves. For readability, Relationship -objects can also be constructed with the `source_ref`, `relationship_type`, and -`target_ref` as positional (non-keyword) arguments: - -```python -relationship = Relationship(indicator, 'indicates', malware) -``` - -### Creating Bundles - -STIX Bundles can be created by passing objects as arguments to the Bundle -constructor. All required properties (`type`, `id`, and `spec_version`) will be -set automatically if not provided, or can be provided as keyword arguments: - -```python -from stix2 import bundle - -bundle = Bundle(indicator, malware, relationship) -``` - -### Serializing STIX objects - -The string representation of all STIX classes is a valid STIX JSON object. - -```python -indicator = Indicator(...) - -print(str(indicator)) -``` - -### Versioning - -TBD - - -## Governance - -This GitHub public repository ( -**** ) was [proposed](https://lists.oasis-open.org/archives/cti/201702/msg00008.html) -and -[approved](https://www.oasis-open.org/committees/download.php/60009/) -\[[bis](https://issues.oasis-open.org/browse/TCADMIN-2549)\] by the [OASIS Cyber Threat Intelligence (CTI) -TC](https://www.oasis-open.org/committees/cti/) as an [OASIS Open -Repository](https://www.oasis-open.org/resources/open-repositories/) to -support development of open source resources related to Technical -Committee work. - -While this Open Repository remains associated with the sponsor TC, its -development priorities, leadership, intellectual property terms, -participation rules, and other matters of governance are [separate and -distinct](https://github.com/oasis-open/cti-python-stix2/blob/master/CONTRIBUTING.md#governance-distinct-from-oasis-tc-process) -from the OASIS TC Process and related policies. - -All contributions made to this Open Repository are subject to open -source license terms expressed in the [BSD-3-Clause -License](https://www.oasis-open.org/sites/www.oasis-open.org/files/BSD-3-Clause.txt). -That license was selected as the declared ["Applicable -License"](https://www.oasis-open.org/resources/open-repositories/licenses) -when the Open Repository was created. - -As documented in ["Public Participation -Invited](https://github.com/oasis-open/cti-python-stix2/blob/master/CONTRIBUTING.md#public-participation-invited)", -contributions to this OASIS Open Repository are invited from all -parties, whether affiliated with OASIS or not. Participants must have a -GitHub account, but no fees or OASIS membership obligations are -required. Participation is expected to be consistent with the [OASIS -Open Repository Guidelines and -Procedures](https://www.oasis-open.org/policies-guidelines/open-repositories), -the open source -[LICENSE](https://github.com/oasis-open/cti-python-stix2/blob/master/LICENSE) -designated for this particular repository, and the requirement for an -[Individual Contributor License -Agreement](https://www.oasis-open.org/resources/open-repositories/cla/individual-cla) -that governs intellectual property. - - -### Maintainers - -Open Repository -[Maintainers](https://www.oasis-open.org/resources/open-repositories/maintainers-guide) -are responsible for oversight of this project's community development -activities, including evaluation of GitHub [pull -requests](https://github.com/oasis-open/cti-python-stix2/blob/master/CONTRIBUTING.md#fork-and-pull-collaboration-model) -and -[preserving](https://www.oasis-open.org/policies-guidelines/open-repositories#repositoryManagement) -open source principles of openness and fairness. Maintainers are -recognized and trusted experts who serve to implement community goals -and consensus design preferences. - -Initially, the associated TC members have designated one or more persons -to serve as Maintainer(s); subsequently, participating community members -may select additional or substitute Maintainers, per [consensus -agreements](https://www.oasis-open.org/resources/open-repositories/maintainers-guide#additionalMaintainers). - -**Current Maintainers of this Open Repository** - -* [Greg Back](mailto:gback@mitre.org); GitHub ID: ; WWW: [MITRE Corporation](http://www.mitre.org/) -* [Chris Lenk](mailto:clenk@mitre.org); GitHub ID: ; WWW: [MITRE Corporation](http://www.mitre.org/) - -## About OASIS Open Repositories - -* [Open Repositories: Overview and Resources](https://www.oasis-open.org/resources/open-repositories/) -* [Frequently Asked Questions](https://www.oasis-open.org/resources/open-repositories/faq) -* [Open Source Licenses](https://www.oasis-open.org/resources/open-repositories/licenses) -* [Contributor License Agreements (CLAs)](https://www.oasis-open.org/resources/open-repositories/cla) -* [Maintainers' Guidelines and Agreement](https://www.oasis-open.org/resources/open-repositories/maintainers-guide) - - -## Feedback - -Questions or comments about this Open Repository's activities should be -composed as GitHub issues or comments. If use of an issue/comment is not -possible or appropriate, questions may be directed by email to the -Maintainer(s) [listed above](#currentMaintainers). Please send general -questions about Open Repository participation to OASIS Staff at - and any specific CLA-related questions -to . diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..b2a8c9c --- /dev/null +++ b/README.rst @@ -0,0 +1,280 @@ +|Build Status| |codecov| + +cti-python-stix2 +================ + +This is an `OASIS Open +Repository `__. +See the `Governance <#governance>`__ section for more information. + +This repository provides Python APIs for serializing and de-serializing +STIX 2 JSON content, along with higher-level APIs for common tasks, +including data markings, versioning, and for resolving STIX IDs across +multiple data sources. + +For more information, see `the +documentation `__ on +ReadTheDocs. + +Installation +------------ + +Install with `pip `__: + +:: + + pip install stix2 + +Usage +----- + +Creating STIX Domain Objects +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To create a STIX object, provide keyword arguments to the type's +constructor: + +.. code:: python + + from stix2 import Indicator + + indicator = Indicator(name="File hash for malware variant", + labels=['malicious-activity'], + pattern='file:hashes.md5 = "d41d8cd98f00b204e9800998ecf8427e"') + +Certain required attributes of all objects will be set automatically if +not provided as keyword arguments: + +- If not provided, ``type`` will be set automatically to the correct + type. You can also provide the type explicitly, but this is not + necessary: + +``python indicator = Indicator(type='indicator', ...)`` + +Passing a value for ``type`` that does not match the class being +constructed will cause an error: + +``python >>> indicator = Indicator(type='xxx', ...) stix2.exceptions.InvalidValueError: Invalid value for Indicator 'type': must equal 'indicator'.`` + +- If not provided, ``id`` will be generated randomly. If you provide an + ``id`` argument, it must begin with the correct prefix: + +``python >>> indicator = Indicator(id="campaign--63ce9068-b5ab-47fa-a2cf-a602ea01f21a") stix2.exceptions.InvalidValueError: Invalid value for Indicator 'id': must start with 'indicator--'.`` + +- If not provided, ``created`` and ``modified`` will be set to the + (same) current time. + +For indicators, ``labels`` and ``pattern`` are required and cannot be +set automatically. Trying to create an indicator that is missing one of +these properties will result in an error: + +.. code:: python + + >>> indicator = Indicator() + stix2.exceptions.MissingPropertiesError: No values for required properties for Indicator: (labels, pattern). + +However, the required ``valid_from`` attribute on Indicators will be set +to the current time if not provided as a keyword argument. + +Once created, the object acts like a frozen dictionary. Properties can +be accessed using the standard Python dictionary syntax: + +.. code:: python + + >>> indicator['name'] + 'File hash for malware variant' + +TBD: Should we allow property access using the standard Python attribute +syntax? + +.. code:: python + + >>> indicator.name + 'File hash for malware variant' + +Attempting to modify any attributes will raise an error: + +.. code:: python + + >>> indicator['name'] = "This is a revised name" + TypeError: 'Indicator' object does not support item assignment + >>> indicator.name = "This is a revised name" + stix2.exceptions.ImmutableError: Cannot modify properties after creation. + +To update the properties of an object, see `Versioning <#versioning>`__ +below. + +Creating a Malware object follows the same pattern: + +.. code:: python + + from stix2 import Malware + + malware = Malware(name="Poison Ivy", + labels=['remote-access-trojan']) + +As with indicators, the ``type``, ``id``, ``created``, and ``modified`` +properties will be set automatically if not provided. For Malware +objects, the ``labels`` and ``name`` properties must be provided. + +Creating Relationships +~~~~~~~~~~~~~~~~~~~~~~ + +STIX 2 Relationships are separate objects, not properties of the object +on either side of the relationship. They are constructed similarly to +other STIX objects. The ``type``, ``id``, ``created``, and ``modified`` +properties are added automatically if not provided. Callers must provide +the ``relationship_type``, ``source_ref``, and ``target_ref`` +properties. + +.. code:: python + + from stix2 import Relationship + + relationship = Relationship(relationship_type='indicates', + source_ref=indicator.id, + target_ref=malware.id) + +The ``source_ref`` and ``target_ref`` properties can be either the ID's +of other STIX objects, or the STIX objects themselves. For readability, +Relationship objects can also be constructed with the ``source_ref``, +``relationship_type``, and ``target_ref`` as positional (non-keyword) +arguments: + +.. code:: python + + relationship = Relationship(indicator, 'indicates', malware) + +Creating Bundles +~~~~~~~~~~~~~~~~ + +STIX Bundles can be created by passing objects as arguments to the +Bundle constructor. All required properties (``type``, ``id``, and +``spec_version``) will be set automatically if not provided, or can be +provided as keyword arguments: + +.. code:: python + + from stix2 import bundle + + bundle = Bundle(indicator, malware, relationship) + +Serializing STIX objects +~~~~~~~~~~~~~~~~~~~~~~~~ + +The string representation of all STIX classes is a valid STIX JSON +object. + +.. code:: python + + indicator = Indicator(...) + + print(str(indicator)) + +Versioning +~~~~~~~~~~ + +TBD + +Governance +---------- + +This GitHub public repository ( +**https://github.com/oasis-open/cti-python-stix2** ) was +`proposed `__ +and +`approved `__ +[`bis `__] by the +`OASIS Cyber Threat Intelligence (CTI) +TC `__ as an `OASIS Open +Repository `__ +to support development of open source resources related to Technical +Committee work. + +While this Open Repository remains associated with the sponsor TC, its +development priorities, leadership, intellectual property terms, +participation rules, and other matters of governance are `separate and +distinct `__ +from the OASIS TC Process and related policies. + +All contributions made to this Open Repository are subject to open +source license terms expressed in the `BSD-3-Clause +License `__. +That license was selected as the declared `"Applicable +License" `__ +when the Open Repository was created. + +As documented in `"Public Participation +Invited `__", +contributions to this OASIS Open Repository are invited from all +parties, whether affiliated with OASIS or not. Participants must have a +GitHub account, but no fees or OASIS membership obligations are +required. Participation is expected to be consistent with the `OASIS +Open Repository Guidelines and +Procedures `__, +the open source +`LICENSE `__ +designated for this particular repository, and the requirement for an +`Individual Contributor License +Agreement `__ +that governs intellectual property. + +Maintainers +~~~~~~~~~~~ + +Open Repository +`Maintainers `__ +are responsible for oversight of this project's community development +activities, including evaluation of GitHub `pull +requests `__ +and +`preserving `__ +open source principles of openness and fairness. Maintainers are +recognized and trusted experts who serve to implement community goals +and consensus design preferences. + +Initially, the associated TC members have designated one or more persons +to serve as Maintainer(s); subsequently, participating community members +may select additional or substitute Maintainers, per `consensus +agreements `__. + +.. _currentMaintainers: + +**Current Maintainers of this Open Repository** + +- `Greg Back `__; GitHub ID: + https://github.com/gtback/; WWW: `MITRE + Corporation `__ +- `Chris Lenk `__; GitHub ID: + https://github.com/clenk/; WWW: `MITRE + Corporation `__ + +About OASIS Open Repositories +----------------------------- + +- `Open Repositories: Overview and + Resources `__ +- `Frequently Asked + Questions `__ +- `Open Source + Licenses `__ +- `Contributor License Agreements + (CLAs) `__ +- `Maintainers' Guidelines and + Agreement `__ + +Feedback +-------- + +Questions or comments about this Open Repository's activities should be +composed as GitHub issues or comments. If use of an issue/comment is not +possible or appropriate, questions may be directed by email to the +Maintainer(s) `listed above <#currentmaintainers>`__. Please send +general questions about Open Repository participation to OASIS Staff at +repository-admin@oasis-open.org and any specific CLA-related questions +to repository-cla@oasis-open.org. + +.. |Build Status| image:: https://travis-ci.org/oasis-open/cti-python-stix2.svg?branch=master + :target: https://travis-ci.org/oasis-open/cti-python-stix2 +.. |codecov| image:: https://codecov.io/gh/oasis-open/cti-python-stix2/branch/master/graph/badge.svg + :target: https://codecov.io/gh/oasis-open/cti-python-stix2 diff --git a/setup.cfg b/setup.cfg index 96a5082..7386629 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,7 +3,7 @@ current_version = 0.2.0 commit = True tag = True -[bumpversion:file:setup.py] +[bumpversion:file:stix2/version.py] [bumpversion:file:docs/conf.py] diff --git a/setup.py b/setup.py index 92b237b..a034bda 100644 --- a/setup.py +++ b/setup.py @@ -1,9 +1,14 @@ #!/usr/bin/env python +from codecs import open +import os.path + from setuptools import find_packages, setup +here = os.path.abspath(os.path.dirname(__file__)) + def get_version(): - with open('stix2/version.py') as f: + with open('stix2/version.py', encoding="utf-8") as f: for line in f.readlines(): if line.startswith("__version__"): version = line.split()[-1].strip('"') @@ -11,19 +16,41 @@ def get_version(): raise AttributeError("Package does not have a __version__") -install_requires = [ - 'pytz', - 'six', - 'python-dateutil', - 'requests', - 'simplejson', -] +with open(os.path.join(here, 'README.rst'), encoding='utf-8') as f: + long_description = f.read() + setup( name='stix2', - description="Produce and consume STIX 2 JSON content", version=get_version(), - packages=find_packages(), - install_requires=install_requires, + description='Produce and consume STIX 2 JSON content', + long_description=long_description, + url='https://github.com/oasis-open/cti-python-stix2', + author='OASIS Cyber Threat Intelligence Technical Committee', + author_email='cti-users@lists.oasis-open.org', + maintainer='Greg Back', + maintainer_email='gback@mitre.org', + license='BSD', + classifiers=[ + 'Development Status :: 4 - Beta', + 'Intended Audience :: Developers', + 'Topic :: Security', + 'License :: OSI Approved :: BSD License', + 'Programming Language :: Python :: 2', + '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', + ], keywords="stix stix2 json cti cyber threat intelligence", + packages=find_packages(), + install_requires=[ + 'pytz', + 'six', + 'python-dateutil', + 'requests', + 'simplejson' + ], ) diff --git a/stix2/__init__.py b/stix2/__init__.py index 98697a9..b9b6764 100644 --- a/stix2/__init__.py +++ b/stix2/__init__.py @@ -3,7 +3,10 @@ # flake8: noqa from . import exceptions -from .bundle import Bundle +from .common import (TLP_AMBER, TLP_GREEN, TLP_RED, TLP_WHITE, + ExternalReference, GranularMarking, KillChainPhase, + MarkingDefinition, StatementMarking, TLPMarking) +from .core import Bundle, _register_type, parse from .environment import ObjectFactory from .observables import (URL, AlternateDataStream, ArchiveExt, Artifact, AutonomousSystem, CustomObservable, Directory, @@ -18,9 +21,6 @@ from .observables import (URL, AlternateDataStream, ArchiveExt, Artifact, WindowsRegistryValueType, WindowsServiceExt, X509Certificate, X509V3ExtenstionsType, parse_observable) -from .other import (TLP_AMBER, TLP_GREEN, TLP_RED, TLP_WHITE, - ExternalReference, GranularMarking, KillChainPhase, - MarkingDefinition, StatementMarking, TLPMarking) from .patterns import (AndBooleanExpression, AndObservationExpression, BasicObjectPathComponent, EqualityComparisonExpression, FloatConstant, FollowedByObservationExpression, @@ -44,51 +44,3 @@ from .sdo import (AttackPattern, Campaign, CourseOfAction, CustomObject, from .sro import Relationship, Sighting from .utils import get_dict from .version import __version__ - -OBJ_MAP = { - 'attack-pattern': AttackPattern, - 'campaign': Campaign, - 'course-of-action': CourseOfAction, - 'identity': Identity, - 'indicator': Indicator, - 'intrusion-set': IntrusionSet, - 'malware': Malware, - 'marking-definition': MarkingDefinition, - 'observed-data': ObservedData, - 'report': Report, - 'relationship': Relationship, - 'threat-actor': ThreatActor, - 'tool': Tool, - 'sighting': Sighting, - 'vulnerability': Vulnerability, -} - - -def parse(data, allow_custom=False): - """Deserialize a string or file-like object into a STIX object. - - Args: - data: The STIX 2 string to be parsed. - allow_custom (bool): Whether to allow custom properties or not. Default: False. - - Returns: - An instantiated Python STIX object. - """ - - obj = get_dict(data) - - if 'type' not in obj: - raise exceptions.ParseError("Can't parse object with no 'type' property: %s" % str(obj)) - - try: - obj_class = OBJ_MAP[obj['type']] - except KeyError: - raise exceptions.ParseError("Can't parse unknown object type '%s'! For custom types, use the CustomObject decorator." % obj['type']) - return obj_class(allow_custom=allow_custom, **obj) - - -def _register_type(new_type): - """Register a custom STIX Object type. - """ - - OBJ_MAP[new_type._type] = new_type diff --git a/stix2/bundle.py b/stix2/bundle.py deleted file mode 100644 index f3d5b2a..0000000 --- a/stix2/bundle.py +++ /dev/null @@ -1,28 +0,0 @@ -"""STIX 2 Bundle object""" - -from collections import OrderedDict - -from .base import _STIXBase -from .properties import IDProperty, Property, TypeProperty - - -class Bundle(_STIXBase): - - _type = 'bundle' - _properties = OrderedDict() - _properties.update([ - ('type', TypeProperty(_type)), - ('id', IDProperty(_type)), - ('spec_version', Property(fixed="2.0")), - ('objects', Property()), - ]) - - def __init__(self, *args, **kwargs): - # Add any positional arguments to the 'objects' kwarg. - if args: - if isinstance(args[0], list): - kwargs['objects'] = args[0] + list(args[1:]) + kwargs.get('objects', []) - else: - kwargs['objects'] = list(args) + kwargs.get('objects', []) - - super(Bundle, self).__init__(**kwargs) diff --git a/stix2/common.py b/stix2/common.py index 638dba5..555ca98 100644 --- a/stix2/common.py +++ b/stix2/common.py @@ -2,10 +2,149 @@ from collections import OrderedDict -from .other import ExternalReference, GranularMarking -from .properties import (BooleanProperty, ListProperty, ReferenceProperty, - StringProperty, TimestampProperty) -from .utils import NOW +from .base import _STIXBase +from .properties import (BooleanProperty, HashesProperty, IDProperty, + ListProperty, Property, ReferenceProperty, + SelectorProperty, StringProperty, TimestampProperty, + TypeProperty) +from .utils import NOW, get_dict + + +class ExternalReference(_STIXBase): + _properties = OrderedDict() + _properties.update([ + ('source_name', StringProperty(required=True)), + ('description', StringProperty()), + ('url', StringProperty()), + ('hashes', HashesProperty()), + ('external_id', StringProperty()), + ]) + + def _check_object_constraints(self): + super(ExternalReference, self)._check_object_constraints() + self._check_at_least_one_property(["description", "external_id", "url"]) + + +class KillChainPhase(_STIXBase): + _properties = OrderedDict() + _properties.update([ + ('kill_chain_name', StringProperty(required=True)), + ('phase_name', StringProperty(required=True)), + ]) + + +class GranularMarking(_STIXBase): + _properties = OrderedDict() + _properties.update([ + ('marking_ref', ReferenceProperty(required=True, type="marking-definition")), + ('selectors', ListProperty(SelectorProperty, required=True)), + ]) + + +class TLPMarking(_STIXBase): + # TODO: don't allow the creation of any other TLPMarkings than the ones below + _type = 'tlp' + _properties = OrderedDict() + _properties.update([ + ('tlp', Property(required=True)) + ]) + + +class StatementMarking(_STIXBase): + _type = 'statement' + _properties = OrderedDict() + _properties.update([ + ('statement', StringProperty(required=True)) + ]) + + def __init__(self, statement=None, **kwargs): + # Allow statement as positional args. + if statement and not kwargs.get('statement'): + kwargs['statement'] = statement + + super(StatementMarking, self).__init__(**kwargs) + + +class MarkingProperty(Property): + """Represent the marking objects in the `definition` property of + marking-definition objects. + """ + + def clean(self, value): + if type(value) in OBJ_MAP_MARKING.values(): + return value + else: + raise ValueError("must be a Statement, TLP Marking or a registered marking.") + + +class MarkingDefinition(_STIXBase): + _type = 'marking-definition' + _properties = OrderedDict() + _properties.update([ + ('type', TypeProperty(_type)), + ('id', IDProperty(_type)), + ('created_by_ref', ReferenceProperty(type="identity")), + ('created', TimestampProperty(default=lambda: NOW)), + ('external_references', ListProperty(ExternalReference)), + ('object_marking_refs', ListProperty(ReferenceProperty(type="marking-definition"))), + ('granular_markings', ListProperty(GranularMarking)), + ('definition_type', StringProperty(required=True)), + ('definition', MarkingProperty(required=True)), + ]) + + def __init__(self, **kwargs): + if set(('definition_type', 'definition')).issubset(kwargs.keys()): + # Create correct marking type object + try: + marking_type = OBJ_MAP_MARKING[kwargs['definition_type']] + except KeyError: + raise ValueError("definition_type must be a valid marking type") + + if not isinstance(kwargs['definition'], marking_type): + defn = get_dict(kwargs['definition']) + kwargs['definition'] = marking_type(**defn) + + super(MarkingDefinition, self).__init__(**kwargs) + + +def register_marking(new_marking): + """Register a custom STIX Marking Definition type. + """ + OBJ_MAP_MARKING[new_marking._type] = new_marking + + +OBJ_MAP_MARKING = { + 'tlp': TLPMarking, + 'statement': StatementMarking, +} + +TLP_WHITE = MarkingDefinition( + id="marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9", + created="2017-01-20T00:00:00.000Z", + definition_type="tlp", + definition=TLPMarking(tlp="white") +) + +TLP_GREEN = MarkingDefinition( + id="marking-definition--34098fce-860f-48ae-8e50-ebd3cc5e41da", + created="2017-01-20T00:00:00.000Z", + definition_type="tlp", + definition=TLPMarking(tlp="green") +) + +TLP_AMBER = MarkingDefinition( + id="marking-definition--f88d31f6-486f-44da-b317-01333bde0b82", + created="2017-01-20T00:00:00.000Z", + definition_type="tlp", + definition=TLPMarking(tlp="amber") +) + +TLP_RED = MarkingDefinition( + id="marking-definition--5e57c739-391a-4eb3-b6be-7d15ca92d5ed", + created="2017-01-20T00:00:00.000Z", + definition_type="tlp", + definition=TLPMarking(tlp="red") +) COMMON_PROPERTIES = OrderedDict() diff --git a/stix2/core.py b/stix2/core.py new file mode 100644 index 0000000..81dd492 --- /dev/null +++ b/stix2/core.py @@ -0,0 +1,99 @@ +"""STIX 2.0 Objects that are neither SDOs nor SROs""" + + +from . import exceptions +from .base import _STIXBase +from .common import MarkingDefinition +from .properties import IDProperty, ListProperty, Property, TypeProperty +from .sdo import (AttackPattern, Campaign, CourseOfAction, Identity, Indicator, + IntrusionSet, Malware, ObservedData, Report, ThreatActor, + Tool, Vulnerability) +from .sro import Relationship, Sighting +from .utils import get_dict + + +class STIXObjectProperty(Property): + + def clean(self, value): + try: + dictified = get_dict(value) + except ValueError: + raise ValueError("This property may only contain a dictionary or object") + if dictified == {}: + raise ValueError("This property may only contain a non-empty dictionary or object") + if 'type' in dictified and dictified['type'] == 'bundle': + raise ValueError('This property may not contain a Bundle object') + + parsed_obj = parse(dictified) + return parsed_obj + + +class Bundle(_STIXBase): + + _type = 'bundle' + _properties = { + 'type': TypeProperty(_type), + 'id': IDProperty(_type), + 'spec_version': Property(fixed="2.0"), + 'objects': ListProperty(STIXObjectProperty), + } + + def __init__(self, *args, **kwargs): + # Add any positional arguments to the 'objects' kwarg. + if args: + if isinstance(args[0], list): + kwargs['objects'] = args[0] + list(args[1:]) + kwargs.get('objects', []) + else: + kwargs['objects'] = list(args) + kwargs.get('objects', []) + + super(Bundle, self).__init__(**kwargs) + + +OBJ_MAP = { + 'attack-pattern': AttackPattern, + 'bundle': Bundle, + 'campaign': Campaign, + 'course-of-action': CourseOfAction, + 'identity': Identity, + 'indicator': Indicator, + 'intrusion-set': IntrusionSet, + 'malware': Malware, + 'marking-definition': MarkingDefinition, + 'observed-data': ObservedData, + 'report': Report, + 'relationship': Relationship, + 'threat-actor': ThreatActor, + 'tool': Tool, + 'sighting': Sighting, + 'vulnerability': Vulnerability, +} + + +def parse(data, allow_custom=False): + """Deserialize a string or file-like object into a STIX object. + + Args: + data: The STIX 2 string to be parsed. + allow_custom (bool): Whether to allow custom properties or not. Default: False. + + Returns: + An instantiated Python STIX object. + """ + + obj = get_dict(data) + + if 'type' not in obj: + raise exceptions.ParseError("Can't parse object with no 'type' property: %s" % str(obj)) + + try: + obj_class = OBJ_MAP[obj['type']] + except KeyError: + raise exceptions.ParseError("Can't parse unknown object type '%s'! For custom types, use the CustomObject decorator." % obj['type']) + return obj_class(allow_custom=allow_custom, **obj) + + +def _register_type(new_type): + """Register a custom STIX Object type. + """ + + OBJ_MAP[new_type._type] = new_type diff --git a/stix2/other.py b/stix2/other.py deleted file mode 100644 index 9659dca..0000000 --- a/stix2/other.py +++ /dev/null @@ -1,145 +0,0 @@ -"""STIX 2.0 Objects that are neither SDOs nor SROs""" - -from collections import OrderedDict - -from .base import _STIXBase -from .properties import (IDProperty, HashesProperty, ListProperty, Property, - ReferenceProperty, SelectorProperty, StringProperty, - TimestampProperty, TypeProperty) -from .utils import NOW, get_dict - - -class ExternalReference(_STIXBase): - _properties = OrderedDict() - _properties.update([ - ('source_name', StringProperty(required=True)), - ('description', StringProperty()), - ('url', StringProperty()), - ('hashes', HashesProperty()), - ('external_id', StringProperty()), - ]) - - def _check_object_constraints(self): - super(ExternalReference, self)._check_object_constraints() - self._check_at_least_one_property(["description", "external_id", "url"]) - - -class KillChainPhase(_STIXBase): - _properties = OrderedDict() - _properties.update([ - ('kill_chain_name', StringProperty(required=True)), - ('phase_name', StringProperty(required=True)), - ]) - - -class GranularMarking(_STIXBase): - _properties = OrderedDict() - _properties.update([ - ('marking_ref', ReferenceProperty(required=True, type="marking-definition")), - ('selectors', ListProperty(SelectorProperty, required=True)), - ]) - - -class TLPMarking(_STIXBase): - _type = 'tlp' - _properties = OrderedDict() - _properties.update([ - ('tlp', Property(required=True)) - ]) - - -class StatementMarking(_STIXBase): - _type = 'statement' - _properties = OrderedDict() - _properties.update([ - ('statement', StringProperty(required=True)) - ]) - - def __init__(self, statement=None, **kwargs): - # Allow statement as positional args. - if statement and not kwargs.get('statement'): - kwargs['statement'] = statement - - super(StatementMarking, self).__init__(**kwargs) - - -class MarkingProperty(Property): - """Represent the marking objects in the `definition` property of - marking-definition objects. - """ - - def clean(self, value): - if type(value) in OBJ_MAP_MARKING.values(): - return value - else: - raise ValueError("must be a Statement, TLP Marking or a registered marking.") - - -class MarkingDefinition(_STIXBase): - _type = 'marking-definition' - _properties = OrderedDict() - _properties.update([ - ('type', TypeProperty(_type)), - ('id', IDProperty(_type)), - ('created_by_ref', ReferenceProperty(type="identity")), - ('created', TimestampProperty(default=lambda: NOW)), - ('external_references', ListProperty(ExternalReference)), - ('object_marking_refs', ListProperty(ReferenceProperty(type="marking-definition"))), - ('granular_markings', ListProperty(GranularMarking)), - ('definition_type', StringProperty(required=True)), - ('definition', MarkingProperty(required=True)), - ]) - - def __init__(self, **kwargs): - if set(('definition_type', 'definition')).issubset(kwargs.keys()): - # Create correct marking type object - try: - marking_type = OBJ_MAP_MARKING[kwargs['definition_type']] - except KeyError: - raise ValueError("definition_type must be a valid marking type") - - if not isinstance(kwargs['definition'], marking_type): - defn = get_dict(kwargs['definition']) - kwargs['definition'] = marking_type(**defn) - - super(MarkingDefinition, self).__init__(**kwargs) - - -def register_marking(new_marking): - """Register a custom STIX Marking Definition type. - """ - OBJ_MAP_MARKING[new_marking._type] = new_marking - - -OBJ_MAP_MARKING = { - 'tlp': TLPMarking, - 'statement': StatementMarking, -} - -TLP_WHITE = MarkingDefinition( - id="marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9", - created="2017-01-20T00:00:00.000Z", - definition_type="tlp", - definition=TLPMarking(tlp="white") -) - -TLP_GREEN = MarkingDefinition( - id="marking-definition--34098fce-860f-48ae-8e50-ebd3cc5e41da", - created="2017-01-20T00:00:00.000Z", - definition_type="tlp", - definition=TLPMarking(tlp="green") -) - -TLP_AMBER = MarkingDefinition( - id="marking-definition--f88d31f6-486f-44da-b317-01333bde0b82", - created="2017-01-20T00:00:00.000Z", - definition_type="tlp", - definition=TLPMarking(tlp="amber") -) - -TLP_RED = MarkingDefinition( - id="marking-definition--5e57c739-391a-4eb3-b6be-7d15ca92d5ed", - created="2017-01-20T00:00:00.000Z", - definition_type="tlp", - definition=TLPMarking(tlp="red") -) diff --git a/stix2/properties.py b/stix2/properties.py index db06763..f63ec8b 100644 --- a/stix2/properties.py +++ b/stix2/properties.py @@ -118,6 +118,9 @@ class ListProperty(Property): if type(self.contained) is EmbeddedObjectProperty: obj_type = self.contained.type + elif type(self.contained).__name__ is 'STIXObjectProperty': + # ^ this way of checking doesn't require a circular import + obj_type = type(valid) else: obj_type = self.contained diff --git a/stix2/sdo.py b/stix2/sdo.py index 75218b7..4904a53 100644 --- a/stix2/sdo.py +++ b/stix2/sdo.py @@ -5,8 +5,8 @@ from collections import OrderedDict import stix2 from .base import _STIXBase +from .common import ExternalReference, GranularMarking, KillChainPhase from .observables import ObservableProperty -from .other import ExternalReference, GranularMarking, KillChainPhase from .properties import (BooleanProperty, IDProperty, IntegerProperty, ListProperty, ReferenceProperty, StringProperty, TimestampProperty, TypeProperty) diff --git a/stix2/test/test_bundle.py b/stix2/test/test_bundle.py index 54d7080..0733637 100644 --- a/stix2/test/test_bundle.py +++ b/stix2/test/test_bundle.py @@ -116,3 +116,15 @@ def test_create_bundle_with_arg_listarg_and_kwarg(indicator, malware, relationsh bundle = stix2.Bundle([indicator], malware, objects=[relationship]) assert str(bundle) == EXPECTED_BUNDLE + + +def test_parse_bundle(): + bundle = stix2.parse(EXPECTED_BUNDLE) + + assert bundle.type == "bundle" + assert bundle.id.startswith("bundle--") + assert bundle.spec_version == "2.0" + assert type(bundle.objects[0]) is stix2.Indicator + assert bundle.objects[0].type == 'indicator' + assert bundle.objects[1].type == 'malware' + assert bundle.objects[2].type == 'relationship'