diff --git a/CHANGELOG b/CHANGELOG index b764735..dc0d91e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,19 @@ CHANGELOG ========= +1.3.1 - 2020-03-06 + +* #322 Adds encoding option FileSystemSource and MemorySource +* #354 Adds ability to specify id-contributing properties on custom SCOs +* #346 Certain SCO properties are no longer deprecated +* #327 Fixes missing 'name' property on Marking Definitions +* #303 Fixes bug with escaping quotes in patterns +* #331 Fixes crashing bug of property names that conflict with Mapping methods +* #337 Fixes bug with detecting STIX version of content when parsing +* #342, #343 Fixes bug when adding SCOs to Memory or FileSystem Stores +* #348 Fixes bug with generating deterministic IDs for SCOs +* #344 Fixes bug with propagating errors from the pattern validator + 1.3.0 - 2020-01-04 * #305 Updates support of STIX 2.1 to WD06 diff --git a/docs/guide/custom.ipynb b/docs/guide/custom.ipynb index 7ceb33b..8185269 100644 --- a/docs/guide/custom.ipynb +++ b/docs/guide/custom.ipynb @@ -175,9 +175,9 @@ ".highlight .vm { color: #19177C } /* Name.Variable.Magic */\n", ".highlight .il { color: #666666 } /* Literal.Number.Integer.Long */
{\n",
        "    "type": "identity",\n",
-       "    "id": "identity--e7fd0fe0-25ff-4fcb-abe5-b6254a9d1a22",\n",
-       "    "created": "2019-07-25T18:18:18.241Z",\n",
-       "    "modified": "2019-07-25T18:18:18.241Z",\n",
+       "    "id": "identity--d6996982-5fb7-4364-b716-b618516989b6",\n",
+       "    "created": "2020-03-05T05:06:27.349Z",\n",
+       "    "modified": "2020-03-05T05:06:27.349Z",\n",
        "    "name": "John Smith",\n",
        "    "identity_class": "individual",\n",
        "    "x_foo": "bar"\n",
@@ -287,9 +287,9 @@
        ".highlight .vm { color: #19177C } /* Name.Variable.Magic */\n",
        ".highlight .il { color: #666666 } /* Literal.Number.Integer.Long */
{\n",
        "    "type": "identity",\n",
-       "    "id": "identity--033b5f42-c94f-488f-8efa-2b6a167f0d6f",\n",
-       "    "created": "2019-07-25T18:18:21.352Z",\n",
-       "    "modified": "2019-07-25T18:18:21.352Z",\n",
+       "    "id": "identity--a167d2de-9fc4-4734-a1ae-57a548aad22a",\n",
+       "    "created": "2020-03-05T05:06:29.180Z",\n",
+       "    "modified": "2020-03-05T05:06:29.180Z",\n",
        "    "name": "John Smith",\n",
        "    "identity_class": "individual",\n",
        "    "x_foo": "bar"\n",
@@ -511,7 +511,7 @@
        "    "type": "identity",\n",
        "    "id": "identity--311b2d2d-f010-4473-83ec-1edf84858f4c",\n",
        "    "created": "2015-12-21T19:59:11.000Z",\n",
-       "    "modified": "2019-07-25T18:18:25.927Z",\n",
+       "    "modified": "2020-03-05T05:06:32.934Z",\n",
        "    "name": "John Smith",\n",
        "    "identity_class": "individual"\n",
        "}\n",
@@ -544,7 +544,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 7,
+   "execution_count": 8,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -569,7 +569,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 8,
+   "execution_count": 9,
    "metadata": {},
    "outputs": [
     {
@@ -645,9 +645,9 @@
        ".highlight .vm { color: #19177C } /* Name.Variable.Magic */\n",
        ".highlight .il { color: #666666 } /* Literal.Number.Integer.Long */
{\n",
        "    "type": "x-animal",\n",
-       "    "id": "x-animal--b1e4fe7f-7985-451d-855c-6ba5c265b22a",\n",
-       "    "created": "2018-04-05T18:38:19.790Z",\n",
-       "    "modified": "2018-04-05T18:38:19.790Z",\n",
+       "    "id": "x-animal--1f7ce0ad-fd3a-4cf0-9cd7-13f7bef9ecd4",\n",
+       "    "created": "2020-03-05T05:06:38.010Z",\n",
+       "    "modified": "2020-03-05T05:06:38.010Z",\n",
        "    "species": "lion",\n",
        "    "animal_class": "mammal"\n",
        "}\n",
@@ -657,7 +657,7 @@
        ""
       ]
      },
-     "execution_count": 8,
+     "execution_count": 9,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -677,7 +677,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 9,
+   "execution_count": 10,
    "metadata": {},
    "outputs": [
     {
@@ -703,7 +703,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 10,
+   "execution_count": 11,
    "metadata": {},
    "outputs": [
     {
@@ -784,7 +784,7 @@
        ""
       ]
      },
-     "execution_count": 10,
+     "execution_count": 11,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -811,7 +811,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 11,
+   "execution_count": 12,
    "metadata": {},
    "outputs": [
     {
@@ -846,7 +846,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 12,
+   "execution_count": 13,
    "metadata": {},
    "outputs": [
     {
@@ -931,7 +931,7 @@
        ""
       ]
      },
-     "execution_count": 12,
+     "execution_count": 13,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -962,7 +962,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 13,
+   "execution_count": 14,
    "metadata": {},
    "outputs": [
     {
@@ -1043,7 +1043,7 @@
        ""
       ]
      },
-     "execution_count": 13,
+     "execution_count": 14,
      "metadata": {},
      "output_type": "execute_result"
     },
@@ -1125,7 +1125,7 @@
        ""
       ]
      },
-     "execution_count": 13,
+     "execution_count": 14,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -1155,6 +1155,316 @@
     "print(obs_data.objects[\"0\"].property_2)"
    ]
   },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### ID-Contributing Properties for Custom Cyber Observables\n",
+    "STIX 2.1 Cyber Observables (SCOs) have deterministic IDs, meaning that the ID of a SCO is based on the values of some of its properties. Thus, if multiple cyber observables of the same type have the same values for their ID-contributing properties, then these SCOs will have the same ID. UUIDv5 is used for the deterministic IDs, using the namespace `\"00abedb4-aa42-466c-9c01-fed23315a9b7\"`. A SCO's ID-contributing properties may consist of a combination of required properties and optional properties.\n",
+    "\n",
+    "If a SCO type does not have any ID contributing properties defined, or all of the ID-contributing properties are not present on the object, then the SCO uses a randomly-generated UUIDv4. Thus, you can optionally define which of your custom SCO's properties should be ID-contributing properties. Similar to standard SCOs, your custom SCO's ID-contributing properties can be any combination of the SCO's required and optional properties.\n",
+    "\n",
+    "You define the ID-contributing properties when defining your custom SCO with the `CustomObservable` decorator. After the list of properties, you can optionally define the list of id-contributing properties. If you do not want to specify any id-contributing properties for your custom SCO, then you do not need to do anything additional.\n",
+    "\n",
+    "See the example below:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 15,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "
{\n",
+       "    "type": "x-new-observable-2",\n",
+       "    "id": "x-new-observable-2--6bc655d6-dcb8-52a3-a862-46848c17e599",\n",
+       "    "a_property": "A property",\n",
+       "    "property_2": 2000\n",
+       "}\n",
+       "
\n" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "text/html": [ + "
{\n",
+       "    "type": "x-new-observable-2",\n",
+       "    "id": "x-new-observable-2--6bc655d6-dcb8-52a3-a862-46848c17e599",\n",
+       "    "a_property": "A property",\n",
+       "    "property_2": 3000\n",
+       "}\n",
+       "
\n" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "text/html": [ + "
{\n",
+       "    "type": "x-new-observable-2",\n",
+       "    "id": "x-new-observable-2--1e56f9c3-a73b-5fbd-b348-83c76523c4df",\n",
+       "    "a_property": "A different property",\n",
+       "    "property_2": 3000\n",
+       "}\n",
+       "
\n" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from stix2.v21 import CustomObservable # IDs and Deterministic IDs are NOT part of STIX 2.0 Custom Observables\n", + "\n", + "@CustomObservable('x-new-observable-2', [\n", + " ('a_property', properties.StringProperty(required=True)),\n", + " ('property_2', properties.IntegerProperty()),\n", + "], [\n", + " 'a_property'\n", + "])\n", + "class NewObservable2():\n", + " pass\n", + "\n", + "new_observable_a = NewObservable2(a_property=\"A property\", property_2=2000)\n", + "print(new_observable_a)\n", + "\n", + "new_observable_b = NewObservable2(a_property=\"A property\", property_2=3000)\n", + "print(new_observable_b)\n", + "\n", + "new_observable_c = NewObservable2(a_property=\"A different property\", property_2=3000)\n", + "print(new_observable_c)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In this example, `a_property` is the only id-contributing property. Notice that the ID for `new_observable_a` and `new_observable_b` is the same since they have the same value for the id-contributing `a_property` property." + ] + }, { "cell_type": "markdown", "metadata": { @@ -1483,21 +1793,21 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "Python 2", "language": "python", - "name": "python3" + "name": "python2" }, "language_info": { "codemirror_mode": { "name": "ipython", - "version": 3 + "version": 2 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.6.3" + "pygments_lexer": "ipython2", + "version": "2.7.15+" } }, "nbformat": 4, diff --git a/setup.cfg b/setup.cfg index 659a1cd..7e89c66 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 1.3.0 +current_version = 1.3.1 commit = True tag = True diff --git a/stix2/custom.py b/stix2/custom.py index a00498b..802fd07 100644 --- a/stix2/custom.py +++ b/stix2/custom.py @@ -53,7 +53,10 @@ def _custom_marking_builder(cls, type, properties, version): return _CustomMarking -def _custom_observable_builder(cls, type, properties, version): +def _custom_observable_builder(cls, type, properties, version, id_contrib_props=None): + if id_contrib_props is None: + id_contrib_props = [] + class _CustomObservable(cls, _Observable): if not re.match(TYPE_REGEX, type): @@ -98,6 +101,8 @@ def _custom_observable_builder(cls, type, properties, version): _type = type _properties = OrderedDict(properties) + if version != '2.0': + _id_contributing_properties = id_contrib_props def __init__(self, **kwargs): _Observable.__init__(self, **kwargs) diff --git a/stix2/datastore/filesystem.py b/stix2/datastore/filesystem.py index e8442e6..d5acc24 100644 --- a/stix2/datastore/filesystem.py +++ b/stix2/datastore/filesystem.py @@ -15,7 +15,7 @@ from stix2.datastore import ( DataSink, DataSource, DataSourceError, DataStoreMixin, ) from stix2.datastore.filters import Filter, FilterSet, apply_common_filters -from stix2.utils import format_datetime, get_type_from_id, is_marking +from stix2.utils import format_datetime, get_type_from_id def _timestamp2filename(timestamp): @@ -329,11 +329,50 @@ def _check_object_from_file(query, filepath, allow_custom, version, encoding): return result +def _is_versioned_type_dir(type_path, type_name): + """ + Try to detect whether the given directory is for a versioned type of STIX + object. This is done by looking for a directory whose name is a STIX ID + of the appropriate type. If found, treat this type as versioned. This + doesn't work when a versioned type directory is empty (it will be + mis-classified as unversioned), but this detection is only necessary when + reading/querying data. If a directory is empty, you'll get no results + either way. + + Args: + type_path: A path to a directory containing one type of STIX object. + type_name: The STIX type name. + + Returns: + True if the directory looks like it contains versioned objects; False + if not. + + Raises: + OSError: If there are errors accessing directory contents or stat()'ing + files + """ + id_regex = re.compile( + r"^" + re.escape(type_name) + + r"--[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}" + r"-[0-9a-f]{12}$", + re.I, + ) + + for entry in os.listdir(type_path): + s = os.stat(os.path.join(type_path, entry)) + if stat.S_ISDIR(s.st_mode) and id_regex.match(entry): + is_versioned = True + break + else: + is_versioned = False + + return is_versioned + + def _search_versioned(query, type_path, auth_ids, allow_custom, version, encoding): """ Searches the given directory, which contains data for STIX objects of a - particular versioned type (i.e. not markings), and return any which match - the query. + particular versioned type, and return any which match the query. Args: query: The query to match against @@ -390,36 +429,24 @@ def _search_versioned(query, type_path, auth_ids, allow_custom, version, encodin # For backward-compatibility, also search for plain files named after # object IDs, in the type directory. - id_files = _get_matching_dir_entries( - type_path, auth_ids, stat.S_ISREG, - ".json", + backcompat_results = _search_unversioned( + query, type_path, auth_ids, allow_custom, version, encoding, ) - for id_file in id_files: - id_path = os.path.join(type_path, id_file) - - try: - stix_obj = _check_object_from_file( - query, id_path, allow_custom, - version, encoding, - ) - if stix_obj: - results.append(stix_obj) - except IOError as e: - if e.errno != errno.ENOENT: - raise - # else, file-not-found is ok, just skip + results.extend(backcompat_results) return results -def _search_markings(query, markings_path, auth_ids, allow_custom, version, encoding): +def _search_unversioned( + query, type_path, auth_ids, allow_custom, version, encoding, +): """ - Searches the given directory, which contains markings data, and return any - which match the query. + Searches the given directory, which contains unversioned data, and return + any objects which match the query. Args: query: The query to match against - markings_path: The directory with STIX markings files + type_path: The directory with STIX files of unversioned type auth_ids: Search optimization based on object ID allow_custom (bool): Whether to allow custom properties as well unknown custom objects. @@ -441,11 +468,11 @@ def _search_markings(query, markings_path, auth_ids, allow_custom, version, enco """ results = [] id_files = _get_matching_dir_entries( - markings_path, auth_ids, stat.S_ISREG, + type_path, auth_ids, stat.S_ISREG, ".json", ) for id_file in id_files: - id_path = os.path.join(markings_path, id_file) + id_path = os.path.join(type_path, id_file) try: stix_obj = _check_object_from_file( @@ -530,12 +557,14 @@ class FileSystemSink(DataSink): """Write the given STIX object to a file in the STIX file directory. """ type_dir = os.path.join(self._stix_dir, stix_obj["type"]) - if is_marking(stix_obj): - filename = stix_obj["id"] - obj_dir = type_dir - else: + + # All versioned objects should have a "modified" property. + if "modified" in stix_obj: filename = _timestamp2filename(stix_obj["modified"]) obj_dir = os.path.join(type_dir, stix_obj["id"]) + else: + filename = stix_obj["id"] + obj_dir = type_dir file_path = os.path.join(obj_dir, filename + ".json") @@ -649,12 +678,14 @@ class FileSystemSource(DataSource): all_data = self.all_versions(stix_id, version=version, _composite_filters=_composite_filters) if all_data: - if is_marking(stix_id): - # Markings are unversioned; there shouldn't be more than one - # result. - stix_obj = all_data[0] - else: + # Simple check for a versioned STIX type: see if the objects have a + # "modified" property. (Need only check one, since they are all of + # the same type.) + is_versioned = "modified" in all_data[0] + if is_versioned: stix_obj = sorted(all_data, key=lambda k: k['modified'])[-1] + else: + stix_obj = all_data[0] else: stix_obj = None @@ -720,14 +751,15 @@ class FileSystemSource(DataSource): ) for type_dir in type_dirs: type_path = os.path.join(self._stix_dir, type_dir) - if type_dir == "marking-definition": - type_results = _search_markings( + type_is_versioned = _is_versioned_type_dir(type_path, type_dir) + if type_is_versioned: + type_results = _search_versioned( query, type_path, auth_ids, self.allow_custom, version, self.encoding, ) else: - type_results = _search_versioned( + type_results = _search_unversioned( query, type_path, auth_ids, self.allow_custom, version, self.encoding, diff --git a/stix2/pattern_visitor.py b/stix2/pattern_visitor.py index b2d7a53..6ac3e98 100644 --- a/stix2/pattern_visitor.py +++ b/stix2/pattern_visitor.py @@ -1,16 +1,12 @@ import importlib import inspect -from antlr4 import CommonTokenStream, InputStream -from antlr4.tree.Trees import Trees -import six from stix2patterns.exceptions import ParseException -from stix2patterns.grammars.STIXPatternLexer import STIXPatternLexer from stix2patterns.grammars.STIXPatternParser import ( STIXPatternParser, TerminalNode, ) from stix2patterns.grammars.STIXPatternVisitor import STIXPatternVisitor -from stix2patterns.validator import STIXPatternErrorListener +from stix2patterns.v20.pattern import Pattern from .patterns import * from .patterns import _BooleanExpression @@ -328,41 +324,9 @@ class STIXPatternVisitorForSTIX2(STIXPatternVisitor): def create_pattern_object(pattern, module_suffix="", module_name=""): """ - Validates a pattern against the STIX Pattern grammar. Error messages are - returned in a list. The test passed if the returned list is empty. + Create a STIX pattern AST from a pattern string. """ - start = '' - if isinstance(pattern, six.string_types): - start = pattern[:2] - pattern = InputStream(pattern) - - if not start: - start = pattern.readline()[:2] - pattern.seek(0) - - parseErrListener = STIXPatternErrorListener() - - lexer = STIXPatternLexer(pattern) - # it always adds a console listener by default... remove it. - lexer.removeErrorListeners() - - stream = CommonTokenStream(lexer) - - parser = STIXPatternParser(stream) - - parser.buildParseTrees = True - # it always adds a console listener by default... remove it. - parser.removeErrorListeners() - parser.addErrorListener(parseErrListener) - - # To improve error messages, replace "" in the literal - # names with symbolic names. This is a hack, but seemed like - # the simplest workaround. - for i, lit_name in enumerate(parser.literalNames): - if lit_name == u"": - parser.literalNames[i] = parser.symbolicNames[i] - - tree = parser.pattern() + pattern_obj = Pattern(pattern) builder = STIXPatternVisitorForSTIX2(module_suffix, module_name) - return builder.visit(tree) + return pattern_obj.visit(builder) diff --git a/stix2/test/v21/stix2_data/directory/directory--572827aa-e0cd-44fd-afd5-a717a7585f39.json b/stix2/test/v21/stix2_data/directory/directory--572827aa-e0cd-44fd-afd5-a717a7585f39.json new file mode 100644 index 0000000..3812ed4 --- /dev/null +++ b/stix2/test/v21/stix2_data/directory/directory--572827aa-e0cd-44fd-afd5-a717a7585f39.json @@ -0,0 +1,11 @@ +{ + "ctime": "2020-10-06T01:54:32.000Z", + "contains_refs": [ + "directory--80539e31-85f3-4304-bd14-e2e8c10859a5", + "file--e9e03175-0357-41b5-a2aa-eb99b455cd0c", + "directory--f6c54233-027b-4464-8126-da1324d8f66c" + ], + "path": "/performance/Democrat.gif", + "type": "directory", + "id": "directory--572827aa-e0cd-44fd-afd5-a717a7585f39" +} diff --git a/stix2/test/v21/test_custom.py b/stix2/test/v21/test_custom.py index 9c650eb..b46288d 100644 --- a/stix2/test/v21/test_custom.py +++ b/stix2/test/v21/test_custom.py @@ -1,3 +1,5 @@ +import uuid + import pytest import stix2 @@ -665,6 +667,76 @@ def test_observed_data_with_custom_observable_object(): assert ob_data.objects['0'].property1 == 'something' +def test_custom_observable_object_det_id_1(): + @stix2.v21.CustomObservable( + 'x-det-id-observable-1', [ + ('property1', stix2.properties.StringProperty(required=True)), + ('property2', stix2.properties.IntegerProperty()), + ], [ + 'property1', + ], + ) + class DetIdObs1(): + pass + + dio_1 = DetIdObs1(property1='I am property1!', property2=42) + dio_2 = DetIdObs1(property1='I am property1!', property2=24) + assert dio_1.property1 == dio_2.property1 == 'I am property1!' + assert dio_1.id == dio_2.id + + uuid_obj = uuid.UUID(dio_1.id[-36:]) + assert uuid_obj.variant == uuid.RFC_4122 + assert uuid_obj.version == 5 + + dio_3 = DetIdObs1(property1='I am property1!', property2=42) + dio_4 = DetIdObs1(property1='I am also property1!', property2=24) + assert dio_3.property1 == 'I am property1!' + assert dio_4.property1 == 'I am also property1!' + assert dio_3.id != dio_4.id + + +def test_custom_observable_object_det_id_2(): + @stix2.v21.CustomObservable( + 'x-det-id-observable-2', [ + ('property1', stix2.properties.StringProperty(required=True)), + ('property2', stix2.properties.IntegerProperty()), + ], [ + 'property1', 'property2', + ], + ) + class DetIdObs2(): + pass + + dio_1 = DetIdObs2(property1='I am property1!', property2=42) + dio_2 = DetIdObs2(property1='I am property1!', property2=42) + assert dio_1.property1 == dio_2.property1 == 'I am property1!' + assert dio_1.property2 == dio_2.property2 == 42 + assert dio_1.id == dio_2.id + + dio_3 = DetIdObs2(property1='I am property1!', property2=42) + dio_4 = DetIdObs2(property1='I am also property1!', property2=42) + assert dio_3.property1 == 'I am property1!' + assert dio_4.property1 == 'I am also property1!' + assert dio_3.property2 == dio_4.property2 == 42 + assert dio_3.id != dio_4.id + + +def test_custom_observable_object_no_id_contrib_props(): + @stix2.v21.CustomObservable( + 'x-det-id-observable-3', [ + ('property1', stix2.properties.StringProperty(required=True)), + ], + ) + class DetIdObs3(): + pass + + dio = DetIdObs3(property1="I am property1!") + + uuid_obj = uuid.UUID(dio.id[-36:]) + assert uuid_obj.variant == uuid.RFC_4122 + assert uuid_obj.version == 4 + + @stix2.v21.CustomExtension( stix2.v21.DomainName, 'x-new-ext', [ ('property1', stix2.properties.StringProperty(required=True)), diff --git a/stix2/test/v21/test_datastore_filesystem.py b/stix2/test/v21/test_datastore_filesystem.py index 9917ccd..3eb8aaa 100644 --- a/stix2/test/v21/test_datastore_filesystem.py +++ b/stix2/test/v21/test_datastore_filesystem.py @@ -221,6 +221,16 @@ def test_filesystem_source_backward_compatible(fs_source): assert result.malware_types == ["version four"] +def test_filesystem_source_sco(fs_source): + results = fs_source.query([stix2.Filter("type", "=", "directory")]) + + assert len(results) == 1 + result = results[0] + assert result["type"] == "directory" + assert result["id"] == "directory--572827aa-e0cd-44fd-afd5-a717a7585f39" + assert result["path"] == "/performance/Democrat.gif" + + def test_filesystem_sink_add_python_stix_object(fs_sink, fs_source): # add python stix object camp1 = stix2.v21.Campaign( @@ -435,6 +445,24 @@ def test_filesystem_sink_marking(fs_sink): os.remove(marking_filepath) +def test_filesystem_sink_sco(fs_sink): + file_sco = { + "type": "file", + "id": "file--decfcc48-31b3-45f5-87c8-1b3a5d71a307", + "name": "cats.png", + } + + fs_sink.add(file_sco) + sco_filepath = os.path.join( + FS_PATH, "file", file_sco["id"] + ".json", + ) + + assert os.path.exists(sco_filepath) + + os.remove(sco_filepath) + os.rmdir(os.path.dirname(sco_filepath)) + + def test_filesystem_store_get_stored_as_bundle(fs_store): coa = fs_store.get("course-of-action--95ddb356-7ba0-4bd9-a889-247262b8946f") assert coa.id == "course-of-action--95ddb356-7ba0-4bd9-a889-247262b8946f" @@ -473,9 +501,10 @@ def test_filesystem_store_query_single_filter(fs_store): def test_filesystem_store_empty_query(fs_store): results = fs_store.query() # returns all - assert len(results) == 30 + assert len(results) == 31 assert "tool--242f3da3-4425-4d11-8f5c-b842886da966" in [obj.id for obj in results] assert "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" in [obj.id for obj in results] + assert "directory--572827aa-e0cd-44fd-afd5-a717a7585f39" in [obj.id for obj in results] def test_filesystem_store_query_multiple_filters(fs_store): @@ -487,7 +516,7 @@ def test_filesystem_store_query_multiple_filters(fs_store): def test_filesystem_store_query_dont_include_type_folder(fs_store): results = fs_store.query(stix2.Filter("type", "!=", "tool")) - assert len(results) == 28 + assert len(results) == 29 def test_filesystem_store_add(fs_store): @@ -574,6 +603,26 @@ def test_filesystem_store_add_marking(fs_store): os.remove(marking_filepath) +def test_filesystem_store_add_sco(fs_store): + sco = stix2.v21.EmailAddress( + value="jdoe@example.com", + ) + + fs_store.add(sco) + sco_filepath = os.path.join( + FS_PATH, "email-addr", sco["id"] + ".json", + ) + + assert os.path.exists(sco_filepath) + + sco_r = fs_store.get(sco["id"]) + assert sco_r["id"] == sco["id"] + assert sco_r["value"] == sco["value"] + + os.remove(sco_filepath) + os.rmdir(os.path.dirname(sco_filepath)) + + def test_filesystem_object_with_custom_property(fs_store): camp = stix2.v21.Campaign( name="Scipio Africanus", @@ -1024,6 +1073,7 @@ def test_search_auth_set_black_empty(rel_fs_store): "attack-pattern", "campaign", "course-of-action", + "directory", "identity", "indicator", "intrusion-set", diff --git a/stix2/test/v21/test_indicator.py b/stix2/test/v21/test_indicator.py index 152f253..d7d7e47 100644 --- a/stix2/test/v21/test_indicator.py +++ b/stix2/test/v21/test_indicator.py @@ -271,7 +271,7 @@ def test_indicator_stix20_invalid_pattern(): ) assert excinfo.value.cls == stix2.v21.Indicator - assert "FAIL: The same qualifier is used more than once" in str(excinfo.value) + assert "FAIL: Duplicate qualifier type encountered: WITHIN" in str(excinfo.value) ind = stix2.v21.Indicator( type="indicator", diff --git a/stix2/test/v21/test_pattern_expressions.py b/stix2/test/v21/test_pattern_expressions.py index 76880be..0c298f8 100644 --- a/stix2/test/v21/test_pattern_expressions.py +++ b/stix2/test/v21/test_pattern_expressions.py @@ -1,6 +1,7 @@ import datetime import pytest +from stix2patterns.exceptions import ParseException import stix2 from stix2.pattern_visitor import create_pattern_object @@ -515,3 +516,8 @@ def test_list_constant(): def test_parsing_multiple_slashes_quotes(): patt_obj = create_pattern_object("[ file:name = 'weird_name\\'' ]") assert str(patt_obj) == "[file:name = 'weird_name\\'']" + + +def test_parse_error(): + with pytest.raises(ParseException): + create_pattern_object("[ file: name = 'weirdname]") diff --git a/stix2/v21/observables.py b/stix2/v21/observables.py index ed560a6..e8c1925 100644 --- a/stix2/v21/observables.py +++ b/stix2/v21/observables.py @@ -966,7 +966,7 @@ class X509Certificate(_Observable): self._check_at_least_one_property(att_list) -def CustomObservable(type='x-custom-observable', properties=None): +def CustomObservable(type='x-custom-observable', properties=None, id_contrib_props=None): """Custom STIX Cyber Observable Object type decorator. Example: @@ -987,7 +987,7 @@ def CustomObservable(type='x-custom-observable', properties=None): properties, [('extensions', ExtensionsProperty(spec_version='2.1', enclosing_type=type))], ])) - return _custom_observable_builder(cls, type, _properties, '2.1') + return _custom_observable_builder(cls, type, _properties, '2.1', id_contrib_props) return wrapper diff --git a/stix2/version.py b/stix2/version.py index 67bc602..9c73af2 100644 --- a/stix2/version.py +++ b/stix2/version.py @@ -1 +1 @@ -__version__ = "1.3.0" +__version__ = "1.3.1"