diff --git a/docs/guide/workbench.ipynb b/docs/guide/workbench.ipynb
new file mode 100644
index 0000000..bc7942a
--- /dev/null
+++ b/docs/guide/workbench.ipynb
@@ -0,0 +1,472 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {
+ "nbsphinx": "hidden"
+ },
+ "outputs": [],
+ "source": [
+ "# Delete this cell to re-enable tracebacks\n",
+ "import sys\n",
+ "ipython = get_ipython()\n",
+ "\n",
+ "def hide_traceback(exc_tuple=None, filename=None, tb_offset=None,\n",
+ " exception_only=False, running_compiled_code=False):\n",
+ " etype, value, tb = sys.exc_info()\n",
+ " return ipython._showtraceback(etype, value, ipython.InteractiveTB.get_exception_only(etype, value))\n",
+ "\n",
+ "ipython.showtraceback = hide_traceback"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 16,
+ "metadata": {
+ "nbsphinx": "hidden"
+ },
+ "outputs": [],
+ "source": [
+ "# JSON output syntax highlighting\n",
+ "from __future__ import print_function\n",
+ "from pygments import highlight\n",
+ "from pygments.lexers import JsonLexer\n",
+ "from pygments.formatters import HtmlFormatter\n",
+ "from six.moves import builtins\n",
+ "from IPython.display import display, HTML\n",
+ "\n",
+ "def json_print(inpt):\n",
+ " string = str(inpt)\n",
+ " if string[0] == '{':\n",
+ " formatter = HtmlFormatter()\n",
+ " display(HTML('{}'.format(\n",
+ " formatter.get_style_defs('.highlight'),\n",
+ " highlight(string, JsonLexer(), formatter))))\n",
+ " else:\n",
+ " builtins.print(inpt)\n",
+ "\n",
+ "globals()['print'] = json_print"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Using A Workbench"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The [Workbench API](../api/stix2.workbench.rst) hides most of the complexity of the rest of the library to make it easy to interact with STIX data. To use it, just import everything from ``stix2.workbench``:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 11,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from stix2.workbench import *"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Retrieving STIX Data\n",
+ "\n",
+ "To get some STIX data to work with, let's set up a DataSource and add it to our workbench."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 12,
+ "metadata": {
+ "scrolled": true
+ },
+ "outputs": [],
+ "source": [
+ "from taxii2client import Collection\n",
+ "\n",
+ "collection = Collection(\"http://127.0.0.1:5000/trustgroup1/collections/91a7b528-80eb-42ed-a74d-c6fbd5a26116/\", user=\"admin\", password=\"Password0\")\n",
+ "tc_source = TAXIICollectionSource(collection)\n",
+ "add_data_source(tc_source)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "collapsed": true
+ },
+ "source": [
+ "Now we can get all of the indicators from the data source."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "response = indicators()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Similar functions are available for the other STIX Object types. See the full list [here](../api/stix2.workbench.rst#stix2.workbench.attack_patterns).\n",
+ "\n",
+ "If you want to only retrieve *some* indicators, you can pass in one or more [Filters](../api/datastore/stix2.datastore.filters.rst). This example finds all the indicators created by a specific identity:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "response = indicators(filters=Filter('created_by_ref', '=', 'identity--adede3e8-bf44-4e6f-b3c9-1958cbc3b188'))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The objects returned let you easily traverse their relationships. Get all Relationship objects involving that object with ``.relationships()``, all other objects related to this object with ``.related()``, and the Identity object for the creator of the object (if one exists) with ``.created_by()``. For full details on these methods and their arguments, see the [Workbench API](../api/stix2.workbench.rst) documentation."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 18,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "indicator--a932fcc6-e032-176c-126f-cb970a5a1ade\n",
+ "indicates\n",
+ "malware--fdd60b30-b67c-11e3-b0b9-f01faf20d111\n"
+ ]
+ }
+ ],
+ "source": [
+ "for i in indicators():\n",
+ " for rel in i.relationships():\n",
+ " print(rel.source_ref)\n",
+ " print(rel.relationship_type)\n",
+ " print(rel.target_ref)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 17,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "
{\n",
+ " "type": "malware",\n",
+ " "id": "malware--fdd60b30-b67c-11e3-b0b9-f01faf20d111",\n",
+ " "created": "2017-01-27T13:49:53.997Z",\n",
+ " "modified": "2017-01-27T13:49:53.997Z",\n",
+ " "name": "Poison Ivy",\n",
+ " "description": "Poison Ivy",\n",
+ " "labels": [\n",
+ " "remote-access-trojan"\n",
+ " ]\n",
+ "}\n",
+ " \n"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "for i in indicators():\n",
+ " for obj in i.related():\n",
+ " print(obj)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "If there are a lot of related objects, you can narrow it down by passing in one or more [Filters](../api/datastore/stix2.datastore.filters.rst) just as before. For example, if we want to get only the indicators related to a specific piece of malware (and not any entities that use it or are targeted by it):"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "{\n",
+ " "type": "indicator",\n",
+ " "id": "indicator--a932fcc6-e032-176c-126f-cb970a5a1ade",\n",
+ " "created": "2014-05-08T09:00:00.000Z",\n",
+ " "modified": "2014-05-08T09:00:00.000Z",\n",
+ " "name": "File hash for Poison Ivy variant",\n",
+ " "pattern": "[file:hashes.'SHA-256' = 'ef537f25c895bfa782526529a9b63d97aa631564d5d789c2b765448c8635fb6c']",\n",
+ " "valid_from": "2014-05-08T09:00:00Z",\n",
+ " "labels": [\n",
+ " "file-hash-watchlist"\n",
+ " ]\n",
+ "}\n",
+ " \n"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 5,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "malware = get('malware--fdd60b30-b67c-11e3-b0b9-f01faf20d111')\n",
+ "indicator = malware.related(filters=Filter('type', '=', 'indicator'))\n",
+ "print(indicator[0])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Creating STIX Data\n",
+ "\n",
+ "To create a STIX object, just use that object's class constructor. Once it's created, add it to the workbench with [save()](../api/datastore/stix2.workbench.rst#stix2.workbench.save)."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "identity = Identity(name=\"ACME Threat Intel Co.\", identity_class=\"organization\")\n",
+ "save(identity)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "You can also set defaults for certain properties when creating objects. For example, let's set the default creator to be the identity object we just created:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "set_default_creator(identity)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Now when we create an indicator (or any other STIX Domain Object), it will automatically have the right ``create_by_ref`` value."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "ACME Threat Intel Co.\n"
+ ]
+ }
+ ],
+ "source": [
+ "indicator = Indicator(labels=[\"malicious-activity\"], pattern=\"[file:hashes.MD5 = 'd41d8cd98f00b204e9800998ecf8427e']\")\n",
+ "save(indicator)\n",
+ "\n",
+ "indicator_creator = get(indicator.created_by_ref)\n",
+ "print(indicator_creator.name)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Defaults can also be set for the [created timestamp](../api/datastore/stix2.workbench.rst#stix2.workbench.set_default_created), [external references](../api/datastore/stix2.workbench.rst#stix2.workbench.set_default_external_refs) and [object marking references](../api/datastore/stix2.workbench.rst#stix2.workbench.set_default_object_marking_refs)."
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.6.3"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/stix2/datastore/__init__.py b/stix2/datastore/__init__.py
index f02d773..9bf23a2 100644
--- a/stix2/datastore/__init__.py
+++ b/stix2/datastore/__init__.py
@@ -181,6 +181,8 @@ class DataStoreMixin(object):
object is the source_ref. Default: False.
target_only (bool): Only examine Relationships for which this
object is the target_ref. Default: False.
+ filters (list): list of additional filters the related objects must
+ match.
Returns:
list: The STIX objects related to the given STIX object.
@@ -194,8 +196,8 @@ class DataStoreMixin(object):
def add(self, *args, **kwargs):
"""Method for storing STIX objects.
- Define custom behavior before storing STIX objects using the associated
- DataSink. Translates add() to the appropriate DataSink call.
+ Defines custom behavior before storing STIX objects using the
+ appropriate method call on the associated DataSink.
Args:
stix_objs (list): a list of STIX objects
@@ -416,7 +418,7 @@ class DataSource(with_metaclass(ABCMeta)):
ids = set()
for r in rels:
ids.update((r.source_ref, r.target_ref))
- ids.remove(obj_id)
+ ids.discard(obj_id)
# Assemble filters
filter_list = []
@@ -678,6 +680,8 @@ class CompositeDataSource(DataSource):
object is the source_ref. Default: False.
target_only (bool): Only examine Relationships for which this
object is the target_ref. Default: False.
+ filters (list): list of additional filters the related objects must
+ match.
Returns:
list: The STIX objects related to the given STIX object.
diff --git a/stix2/test/test_workbench.py b/stix2/test/test_workbench.py
index a2016cc..7857eb2 100644
--- a/stix2/test/test_workbench.py
+++ b/stix2/test/test_workbench.py
@@ -6,12 +6,12 @@ from stix2.workbench import (AttackPattern, Campaign, CourseOfAction,
Identity, Indicator, IntrusionSet, Malware,
MarkingDefinition, ObservedData, Relationship,
Report, StatementMarking, ThreatActor, Tool,
- Vulnerability, add, add_data_source, all_versions,
+ Vulnerability, add_data_source, all_versions,
attack_patterns, campaigns, courses_of_action,
create, get, identities, indicators,
intrusion_sets, malware, observed_data, query,
- reports, set_default_created, set_default_creator,
- set_default_external_refs,
+ reports, save, set_default_created,
+ set_default_creator, set_default_external_refs,
set_default_object_marking_refs, threat_actors,
tools, vulnerabilities)
@@ -30,7 +30,7 @@ def test_workbench_environment():
# Create a STIX object
ind = create(Indicator, id=INDICATOR_ID, **INDICATOR_KWARGS)
- add(ind)
+ save(ind)
resp = get(INDICATOR_ID)
assert resp['labels'][0] == 'malicious-activity'
@@ -46,7 +46,7 @@ def test_workbench_environment():
def test_workbench_get_all_attack_patterns():
mal = AttackPattern(id=ATTACK_PATTERN_ID, **ATTACK_PATTERN_KWARGS)
- add(mal)
+ save(mal)
resp = attack_patterns()
assert len(resp) == 1
@@ -55,7 +55,7 @@ def test_workbench_get_all_attack_patterns():
def test_workbench_get_all_campaigns():
cam = Campaign(id=CAMPAIGN_ID, **CAMPAIGN_KWARGS)
- add(cam)
+ save(cam)
resp = campaigns()
assert len(resp) == 1
@@ -64,7 +64,7 @@ def test_workbench_get_all_campaigns():
def test_workbench_get_all_courses_of_action():
coa = CourseOfAction(id=COURSE_OF_ACTION_ID, **COURSE_OF_ACTION_KWARGS)
- add(coa)
+ save(coa)
resp = courses_of_action()
assert len(resp) == 1
@@ -73,7 +73,7 @@ def test_workbench_get_all_courses_of_action():
def test_workbench_get_all_identities():
idty = Identity(id=IDENTITY_ID, **IDENTITY_KWARGS)
- add(idty)
+ save(idty)
resp = identities()
assert len(resp) == 1
@@ -88,7 +88,7 @@ def test_workbench_get_all_indicators():
def test_workbench_get_all_intrusion_sets():
ins = IntrusionSet(id=INTRUSION_SET_ID, **INTRUSION_SET_KWARGS)
- add(ins)
+ save(ins)
resp = intrusion_sets()
assert len(resp) == 1
@@ -97,7 +97,7 @@ def test_workbench_get_all_intrusion_sets():
def test_workbench_get_all_malware():
mal = Malware(id=MALWARE_ID, **MALWARE_KWARGS)
- add(mal)
+ save(mal)
resp = malware()
assert len(resp) == 1
@@ -106,7 +106,7 @@ def test_workbench_get_all_malware():
def test_workbench_get_all_observed_data():
od = ObservedData(id=OBSERVED_DATA_ID, **OBSERVED_DATA_KWARGS)
- add(od)
+ save(od)
resp = observed_data()
assert len(resp) == 1
@@ -115,7 +115,7 @@ def test_workbench_get_all_observed_data():
def test_workbench_get_all_reports():
rep = Report(id=REPORT_ID, **REPORT_KWARGS)
- add(rep)
+ save(rep)
resp = reports()
assert len(resp) == 1
@@ -124,7 +124,7 @@ def test_workbench_get_all_reports():
def test_workbench_get_all_threat_actors():
thr = ThreatActor(id=THREAT_ACTOR_ID, **THREAT_ACTOR_KWARGS)
- add(thr)
+ save(thr)
resp = threat_actors()
assert len(resp) == 1
@@ -133,7 +133,7 @@ def test_workbench_get_all_threat_actors():
def test_workbench_get_all_tools():
tool = Tool(id=TOOL_ID, **TOOL_KWARGS)
- add(tool)
+ save(tool)
resp = tools()
assert len(resp) == 1
@@ -142,7 +142,7 @@ def test_workbench_get_all_tools():
def test_workbench_get_all_vulnerabilities():
vuln = Vulnerability(id=VULNERABILITY_ID, **VULNERABILITY_KWARGS)
- add(vuln)
+ save(vuln)
resp = vulnerabilities()
assert len(resp) == 1
@@ -151,7 +151,7 @@ def test_workbench_get_all_vulnerabilities():
def test_workbench_relationships():
rel = Relationship(INDICATOR_ID, 'indicates', MALWARE_ID)
- add(rel)
+ save(rel)
ind = get(INDICATOR_ID)
resp = ind.relationships()
@@ -163,7 +163,7 @@ def test_workbench_relationships():
def test_workbench_created_by():
intset = IntrusionSet(name="Breach 123", created_by_ref=IDENTITY_ID)
- add(intset)
+ save(intset)
creator = intset.created_by()
assert creator.id == IDENTITY_ID
@@ -171,7 +171,7 @@ def test_workbench_created_by():
def test_workbench_related():
rel1 = Relationship(MALWARE_ID, 'targets', IDENTITY_ID)
rel2 = Relationship(CAMPAIGN_ID, 'uses', MALWARE_ID)
- add([rel1, rel2])
+ save([rel1, rel2])
resp = get(MALWARE_ID).related()
assert len(resp) == 3
@@ -186,7 +186,7 @@ def test_workbench_related():
def test_workbench_related_with_filters():
malware = Malware(labels=["ransomware"], name="CryptorBit", created_by_ref=IDENTITY_ID)
rel = Relationship(malware.id, 'variant-of', MALWARE_ID)
- add([malware, rel])
+ save([malware, rel])
filters = [Filter('created_by_ref', '=', IDENTITY_ID)]
resp = get(MALWARE_ID).related(filters=filters)
diff --git a/stix2/workbench.py b/stix2/workbench.py
index f57b5f4..0338353 100644
--- a/stix2/workbench.py
+++ b/stix2/workbench.py
@@ -12,7 +12,7 @@
.. autofunction:: creator_of
.. autofunction:: relationships
.. autofunction:: related_to
-.. autofunction:: add
+.. autofunction:: save
.. autofunction:: add_filters
.. autofunction:: add_filter
.. autofunction:: parse
@@ -65,7 +65,7 @@ query_by_type = _environ.query_by_type
creator_of = _environ.creator_of
relationships = _environ.relationships
related_to = _environ.related_to
-add = _environ.add
+save = _environ.add
add_filters = _environ.add_filters
add_filter = _environ.add_filter
parse = _environ.parse
@@ -132,7 +132,7 @@ def _setup_workbench():
for obj_type in STIX_OBJS:
new_class_dict = {
'__new__': _constructor_wrapper(obj_type),
- '__doc__': 'Workbench wrapper around the `{0} `__. object. {1}'.format(obj_type.__name__, STIX_OBJ_DOCS)
+ '__doc__': 'Workbench wrapper around the `{0} `__ object. {1}'.format(obj_type.__name__, STIX_OBJ_DOCS)
}
new_class = type(obj_type.__name__, (), new_class_dict)