1"""STIX 2.1 Common Data Types and Properties."""
2
3from collections import OrderedDict
4
5from ..custom import _custom_marking_builder
6from ..exceptions import InvalidValueError
7from ..markings import _MarkingsMixin
8from ..markings.utils import check_tlp_marking
9from ..properties import (
10    BooleanProperty, DictionaryProperty, HashesProperty, IDProperty,
11    IntegerProperty, ListProperty, Property, ReferenceProperty,
12    SelectorProperty, StringProperty, TimestampProperty, TypeProperty,
13)
14from ..utils import NOW, _get_dict
15from .base import _STIXBase21
16
17
18class ExternalReference(_STIXBase21):
19    """For more detailed information on this object's properties, see
20    `the STIX 2.1 specification <https://docs.oasis-open.org/cti/stix/v2.1/cs01/stix-v2.1-cs01.html#_bajcvqteiard>`__.
21    """
22
23    _properties = OrderedDict([
24        ('source_name', StringProperty(required=True)),
25        ('description', StringProperty()),
26        ('url', StringProperty()),
27        ('hashes', HashesProperty(spec_version='2.1')),
28        ('external_id', StringProperty()),
29    ])
30
31    # This is hash-algorithm-ov
32    _LEGAL_HASHES = {
33        "MD5", "SHA-1", "SHA-256", "SHA-512", "SHA3-256", "SHA3-512", "SSDEEP",
34        "TLSH",
35    }
36
37    def _check_object_constraints(self):
38        super(ExternalReference, self)._check_object_constraints()
39        self._check_at_least_one_property(['description', 'external_id', 'url'])
40
41        if "hashes" in self:
42            if any(
43                hash_ not in self._LEGAL_HASHES
44                for hash_ in self["hashes"]
45            ):
46                raise InvalidValueError(
47                    ExternalReference, "hashes",
48                    "Hash algorithm names must be members of hash-algorithm-ov",
49                )
50
51
52class KillChainPhase(_STIXBase21):
53    """For more detailed information on this object's properties, see
54    `the STIX 2.1 specification <https://docs.oasis-open.org/cti/stix/v2.1/cs01/stix-v2.1-cs01.html#_i4tjv75ce50h>`__.
55    """
56
57    _properties = OrderedDict([
58        ('kill_chain_name', StringProperty(required=True)),
59        ('phase_name', StringProperty(required=True)),
60    ])
61
62
63class GranularMarking(_STIXBase21):
64    """For more detailed information on this object's properties, see
65    `the STIX 2.1 specification <https://docs.oasis-open.org/cti/stix/v2.1/cs01/stix-v2.1-cs01.html#_robezi5egfdr>`__.
66    """
67
68    _properties = OrderedDict([
69        ('lang', StringProperty()),
70        ('marking_ref', ReferenceProperty(valid_types='marking-definition', spec_version='2.1')),
71        ('selectors', ListProperty(SelectorProperty, required=True)),
72    ])
73
74    def _check_object_constraints(self):
75        super(GranularMarking, self)._check_object_constraints()
76        self._check_at_least_one_property(['lang', 'marking_ref'])
77
78
79class LanguageContent(_STIXBase21):
80    """For more detailed information on this object's properties, see
81    `the STIX 2.1 specification <https://docs.oasis-open.org/cti/stix/v2.1/cs01/stix-v2.1-cs01.html#_nfwr8z9ax2bi>`__.
82    """
83
84    _type = 'language-content'
85    _properties = OrderedDict([
86        ('type', TypeProperty(_type, spec_version='2.1')),
87        ('spec_version', StringProperty(fixed='2.1')),
88        ('id', IDProperty(_type, spec_version='2.1')),
89        ('created_by_ref', ReferenceProperty(valid_types='identity', spec_version='2.1')),
90        ('created', TimestampProperty(default=lambda: NOW, precision='millisecond', precision_constraint='min')),
91        ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond', precision_constraint='min')),
92        ('object_ref', ReferenceProperty(valid_types=["SCO", "SDO", "SRO"], spec_version='2.1', required=True)),
93        # TODO: 'object_modified' it MUST be an exact match for the modified time of the STIX Object (SRO or SDO) being referenced.
94        ('object_modified', TimestampProperty(precision='millisecond')),
95        # TODO: 'contents' https://docs.google.com/document/d/1ShNq4c3e1CkfANmD9O--mdZ5H0O_GLnjN28a_yrEaco/edit#heading=h.cfz5hcantmvx
96        ('contents', DictionaryProperty(spec_version='2.1', required=True)),
97        ('revoked', BooleanProperty(default=lambda: False)),
98        ('labels', ListProperty(StringProperty)),
99        ('confidence', IntegerProperty()),
100        ('external_references', ListProperty(ExternalReference)),
101        ('object_marking_refs', ListProperty(ReferenceProperty(valid_types='marking-definition', spec_version='2.1'))),
102        ('granular_markings', ListProperty(GranularMarking)),
103    ])
104
105
106class TLPMarking(_STIXBase21):
107    """For more detailed information on this object's properties, see
108    `the STIX 2.1 specification <https://docs.oasis-open.org/cti/stix/v2.1/cs01/stix-v2.1-cs01.html#_yd3ar14ekwrs>`__.
109    """
110
111    _type = 'tlp'
112    _properties = OrderedDict([
113        ('tlp', StringProperty(required=True)),
114    ])
115
116
117class StatementMarking(_STIXBase21):
118    """For more detailed information on this object's properties, see
119    `the STIX 2.1 specification <https://docs.oasis-open.org/cti/stix/v2.1/cs01/stix-v2.1-cs01.html#_3ru8r05saera>`__.
120    """
121
122    _type = 'statement'
123    _properties = OrderedDict([
124        ('statement', StringProperty(required=True)),
125    ])
126
127    def __init__(self, statement=None, **kwargs):
128        # Allow statement as positional args.
129        if statement and not kwargs.get('statement'):
130            kwargs['statement'] = statement
131
132        super(StatementMarking, self).__init__(**kwargs)
133
134
135class MarkingProperty(Property):
136    """Represent the marking objects in the ``definition`` property of
137    marking-definition objects.
138    """
139
140    def clean(self, value):
141        if type(value) in OBJ_MAP_MARKING.values():
142            return value
143        else:
144            raise ValueError("must be a Statement, TLP Marking or a registered marking.")
145
146
147class MarkingDefinition(_STIXBase21, _MarkingsMixin):
148    """For more detailed information on this object's properties, see
149    `the STIX 2.1 specification <https://docs.oasis-open.org/cti/stix/v2.1/cs01/stix-v2.1-cs01.html#_hr5vgqxjk7ns>`__.
150    """
151
152    _type = 'marking-definition'
153    _properties = OrderedDict([
154        ('type', TypeProperty(_type, spec_version='2.1')),
155        ('spec_version', StringProperty(fixed='2.1')),
156        ('id', IDProperty(_type)),
157        ('created_by_ref', ReferenceProperty(valid_types='identity', spec_version='2.1')),
158        ('created', TimestampProperty(default=lambda: NOW, precision='millisecond', precision_constraint='min')),
159        ('definition_type', StringProperty(required=True)),
160        ('name', StringProperty()),
161        ('definition', MarkingProperty(required=True)),
162        ('external_references', ListProperty(ExternalReference)),
163        ('object_marking_refs', ListProperty(ReferenceProperty(valid_types='marking-definition', spec_version='2.1'))),
164        ('granular_markings', ListProperty(GranularMarking)),
165    ])
166
167    def __init__(self, **kwargs):
168        if set(('definition_type', 'definition')).issubset(kwargs.keys()):
169            # Create correct marking type object
170            try:
171                marking_type = OBJ_MAP_MARKING[kwargs['definition_type']]
172            except KeyError:
173                raise ValueError("definition_type must be a valid marking type")
174
175            if not isinstance(kwargs['definition'], marking_type):
176                defn = _get_dict(kwargs['definition'])
177                kwargs['definition'] = marking_type(**defn)
178
179        super(MarkingDefinition, self).__init__(**kwargs)
180
181    def _check_object_constraints(self):
182        super(MarkingDefinition, self)._check_object_constraints()
183        check_tlp_marking(self, '2.1')
184
185    def serialize(self, pretty=False, include_optional_defaults=False, **kwargs):
186        check_tlp_marking(self, '2.1')
187        return super(MarkingDefinition, self).serialize(pretty, include_optional_defaults, **kwargs)
188
189
190OBJ_MAP_MARKING = {
191    'tlp': TLPMarking,
192    'statement': StatementMarking,
193}
194
195
196def CustomMarking(type='x-custom-marking', properties=None):
197    """Custom STIX Marking decorator.
198
199    Example:
200        >>> from stix2.v21 import CustomMarking
201        >>> from stix2.properties import IntegerProperty, StringProperty
202        >>> @CustomMarking('x-custom-marking', [
203        ...     ('property1', StringProperty(required=True)),
204        ...     ('property2', IntegerProperty()),
205        ... ])
206        ... class MyNewMarkingObjectType():
207        ...     pass
208
209    """
210    def wrapper(cls):
211        return _custom_marking_builder(cls, type, properties, '2.1', _STIXBase21)
212    return wrapper
213
214
215# TODO: don't allow the creation of any other TLPMarkings than the ones below
216
217TLP_WHITE = MarkingDefinition(
218    id='marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9',
219    created='2017-01-20T00:00:00.000Z',
220    definition_type='tlp',
221    name='TLP:WHITE',
222    definition=TLPMarking(tlp='white'),
223)
224
225TLP_GREEN = MarkingDefinition(
226    id='marking-definition--34098fce-860f-48ae-8e50-ebd3cc5e41da',
227    created='2017-01-20T00:00:00.000Z',
228    definition_type='tlp',
229    name='TLP:GREEN',
230    definition=TLPMarking(tlp='green'),
231)
232
233TLP_AMBER = MarkingDefinition(
234    id='marking-definition--f88d31f6-486f-44da-b317-01333bde0b82',
235    created='2017-01-20T00:00:00.000Z',
236    definition_type='tlp',
237    name='TLP:AMBER',
238    definition=TLPMarking(tlp='amber'),
239)
240
241TLP_RED = MarkingDefinition(
242    id='marking-definition--5e57c739-391a-4eb3-b6be-7d15ca92d5ed',
243    created='2017-01-20T00:00:00.000Z',
244    definition_type='tlp',
245    name='TLP:RED',
246    definition=TLPMarking(tlp='red'),
247)
248