From 8b2e01771ce0c6f71193b9a5162edf6a8c813951 Mon Sep 17 00:00:00 2001 From: clittle Date: Fri, 9 Oct 2020 10:44:40 -0400 Subject: [PATCH 1/7] Updated filters to support version 4 of the layer format. Documentation has not been updated yet. --- layers/core/filter.py | 32 ++++++++++++++++++-------------- layers/core/layerobj.py | 12 ++++++++---- 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/layers/core/filter.py b/layers/core/filter.py index 5b36a8aa..fc53674b 100644 --- a/layers/core/filter.py +++ b/layers/core/filter.py @@ -6,7 +6,7 @@ UNSETVALUE -class Filter: +class Filterv4: def __init__(self, domain="mitre-enterprise"): """ Initialization - Creates a filter object, with an optional @@ -15,22 +15,9 @@ def __init__(self, domain="mitre-enterprise"): :param domain: The domain used for this layer (mitre-enterprise or mitre-mobile) """ - self.__stages = UNSETVALUE self.domain = domain self.__platforms = UNSETVALUE - @property - def stages(self): - if self.__stages != UNSETVALUE: - return self.__stages - - @stages.setter - def stages(self, stage): - typeCheckerArray(type(self).__name__, stage, str, "stage") - categoryChecker(type(self).__name__, stage[0], ["act", "prepare"], - "stages") - self.__stages = stage - @property def platforms(self): if self.__platforms != UNSETVALUE: @@ -63,3 +50,20 @@ def get_dict(self): = listing[entry] if len(temp) > 0: return temp + +class Filter(Filterv4): + def __init__(self, domain="mitre-enterprise"): + self.__stages = UNSETVALUE + super().__init__(domain) + + @property + def stages(self): + if self.__stages != UNSETVALUE: + return self.__stages + + @stages.setter + def stages(self, stage): + typeCheckerArray(type(self).__name__, stage, str, "stage") + categoryChecker(type(self).__name__, stage[0], ["act", "prepare"], + "stages") + self.__stages = stage \ No newline at end of file diff --git a/layers/core/layerobj.py b/layers/core/layerobj.py index dda4a6a5..374aae37 100644 --- a/layers/core/layerobj.py +++ b/layers/core/layerobj.py @@ -1,5 +1,5 @@ try: - from ..core.filter import Filter + from ..core.filter import Filter, Filterv4 from ..core.layout import Layout from ..core.technique import Technique from ..core.gradient import Gradient @@ -51,7 +51,7 @@ def version(self): @version.setter def version(self, version): typeChecker(type(self).__name__, version, str, "version") - categoryChecker(type(self).__name__, version, ["3.0"], "version") + categoryChecker(type(self).__name__, version, ["3.0", "4.0"], "version") self.__version = version @property @@ -92,9 +92,13 @@ def filters(self): @filters.setter def filters(self, filters): - try: + if self.version == "4.0": + temp = Filterv4(self.domain) + else: temp = Filter(self.domain) - temp.stages = filters['stages'] + try: + if self.version != "4.0": + temp.stages = filters['stages'] temp.platforms = filters['platforms'] self.__filters = temp except KeyError as e: From c18ab47b750d4c53024ec9883a49bff634f48440 Mon Sep 17 00:00:00 2001 From: clittle Date: Wed, 14 Oct 2020 11:58:21 -0400 Subject: [PATCH 2/7] Added support for versions and domain changes in v4 --- layers/README.md | 14 +++++++--- layers/core/filter.py | 5 ++-- layers/core/layer.py | 14 ++++++++-- layers/core/layerobj.py | 34 +++++++++++++++++++----- layers/core/versions.py | 59 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 112 insertions(+), 14 deletions(-) create mode 100644 layers/core/versions.py diff --git a/layers/README.md b/layers/README.md index ef5e92a5..8341682a 100644 --- a/layers/README.md +++ b/layers/README.md @@ -12,7 +12,7 @@ This folder contains modules and scripts for working with ATT&CK Navigator layer | [legenditem](core/legenditem.py) | Implements a basic [legenditem object](https://github.com/mitre-attack/attack-navigator/blob/develop/layers/LAYERFORMATv3.md#legenditem-object-properties). | | [metadata](core/metadata.py) | Implements a basic [metadata object](https://github.com/mitre-attack/attack-navigator/blob/develop/layers/LAYERFORMATv3.md#metadata-object-properties). | | [technique](core/technique.py) | Implements a basic [technique object](https://github.com/mitre-attack/attack-navigator/blob/develop/layers/LAYERFORMATv3.md#technique-object-properties). | - +| [version](core/versions.py) | Impelments a basic [version object]().| #### Manipulator Scripts | script | description | |:-------|:------------| @@ -36,7 +36,7 @@ This folder contains modules and scripts for working with ATT&CK Navigator layer | [layerExporter_cli.py](layerExporter_cli.py) | A commandline utility to export Layer files to excel or svg formats using the exporter tools. Run with `-h` for usage. | ## Layer -The Layer class provides format validation and read/write capabilities to aid in working with ATT&CK Navigator Layers in python. It is the primary interface through which other Layer-related classes defined in the core module should be used. The Layer class API and a usage example are below. +The Layer class provides format validation and read/write capabilities to aid in working with ATT&CK Navigator Layers in python. It is the primary interface through which other Layer-related classes defined in the core module should be used. The Layer class API and a usage example are below. The class currently supports version 3 and 4 of the ATT&CK Layer spec, and will upgrade version 3 layers into compatible version 4 ones whenever possible. | method [x = Layer()]| description | |:-------|:------------| @@ -56,6 +56,14 @@ example_layer_dict = { "domain": "mitre-enterprise" } +example_layer4_dict = { + "name": "layer v4 example", + "versions" : { + "layer" : "4.0" + }, + "domain": "enterprise-attack" +} + example_layer_location = "/path/to/layer/file.json" example_layer_out_location = "/path/to/new/layer/file.json" @@ -65,7 +73,7 @@ layer1 = Layer(example_layer_dict) # Create a new layer and load ex layer1.to_file(example_layer_out_location) # Write out the loaded layer to the specified file layer2 = Layer() # Create a new layer object -layer2.from_dict(example_layer_dict) # Load layer data into existing layer object +layer2.from_dict(example_layer4_dict) # Load layer data into existing layer object print(layer2.to_dict()) # Retrieve the loaded layer's data as a dictionary, and print it layer3 = Layer() # Create a new layer object diff --git a/layers/core/filter.py b/layers/core/filter.py index fc53674b..d99973e7 100644 --- a/layers/core/filter.py +++ b/layers/core/filter.py @@ -46,8 +46,9 @@ def get_dict(self): if entry == 'domain': continue if listing[entry] != UNSETVALUE: - temp[entry.split(type(self).__name__ + '__')[-1]] \ - = listing[entry] + subname = entry.split('__')[-1] + if subname != 'stages': + temp[subname] = listing[entry] if len(temp) > 0: return temp diff --git a/layers/core/layer.py b/layers/core/layer.py index cb17a099..e7c226a9 100644 --- a/layers/core/layer.py +++ b/layers/core/layer.py @@ -75,7 +75,17 @@ def _build(self): Loads the data stored in self.data into a LayerObj (self.layer) """ try: - self.__layer = _LayerObj(self._data['version'], self._data['name'], + if 'version' in self._data: + ver_obj = dict(layer=self._data['version']) + else: + ver_obj = self._data['versions'] + except: + handler(type(self).__name__, 'Layer version is malformed. ' + 'Unable to load') + self.__layer = None + return + try: + self.__layer = _LayerObj(ver_obj, self._data['name'], self._data['domain']) except BadType or BadInput as e: handler(type(self).__name__, 'Layer is malformed: {}. ' @@ -89,7 +99,7 @@ def _build(self): return for key in self._data: - if key not in ['version', 'name', 'domain']: + if key not in ['version', 'versions', 'name', 'domain']: try: self.__layer._linker(key, self._data[key]) except Exception as e: diff --git a/layers/core/layerobj.py b/layers/core/layerobj.py index 374aae37..ae2f8824 100644 --- a/layers/core/layerobj.py +++ b/layers/core/layerobj.py @@ -5,6 +5,7 @@ from ..core.gradient import Gradient from ..core.legenditem import LegendItem from ..core.metadata import Metadata + from ..core.versions import Versions from ..core.exceptions import UNSETVALUE, typeChecker, BadInput, handler, \ categoryChecker, UnknownLayerProperty except ValueError: @@ -14,11 +15,12 @@ from core.gradient import Gradient from core.legenditem import LegendItem from core.metadata import Metadata + from core.versions import Versions from core.exceptions import UNSETVALUE, typeChecker, BadInput, handler, \ categoryChecker, UnknownLayerProperty class _LayerObj: - def __init__(self, version, name, domain): + def __init__(self, versions, name, domain): """ Initialization - Creates a layer object @@ -27,7 +29,7 @@ def __init__(self, version, name, domain): :param domain: The domain for this layer (mitre-enterprise or mitre-mobile) """ - self.version = version + self.versions = versions self.name = name self.__description = UNSETVALUE self.domain = domain @@ -46,13 +48,28 @@ def __init__(self, version, name, domain): @property def version(self): - return self.__version + return self.__versions.layer @version.setter def version(self, version): typeChecker(type(self).__name__, version, str, "version") categoryChecker(type(self).__name__, version, ["3.0", "4.0"], "version") - self.__version = version + self.__versions.layer = version + + @property + def versions(self): + return self.__versions + + @versions.setter + def versions(self, versions): + typeChecker(type(self).__name__, versions, dict, "version") + attack = None + nav = None + if 'attack' in versions: + attack = versions['attack'] + if 'navigator' in versions: + nav = versions['navigator'] + self.__versions = Versions(versions['layer'], attack, nav) @property def name(self): @@ -70,8 +87,11 @@ def domain(self): @domain.setter def domain(self, domain): typeChecker(type(self).__name__, domain, str, "domain") - categoryChecker(type(self).__name__, domain, ["mitre-enterprise", - "mitre-mobile"], + dom = domain + if dom.startswith('mitre'): + dom = dom.split('-')[-1] + '-attack' + categoryChecker(type(self).__name__, dom, ["enterprise-attack", + "mobile-attack"], "domain") self.__domain = domain @@ -301,7 +321,7 @@ def get_dict(self): Converts the currently loaded layer into a dict :returns: A dict representation of the current layer object """ - temp = dict(name=self.name, version=self.version, domain=self.domain) + temp = dict(name=self.name, versions=self.versions.get_dict(), domain=self.domain) if self.description: temp['description'] = self.description diff --git a/layers/core/versions.py b/layers/core/versions.py new file mode 100644 index 00000000..a9f32670 --- /dev/null +++ b/layers/core/versions.py @@ -0,0 +1,59 @@ +try: + from ..core.exceptions import typeChecker, categoryChecker, UNSETVALUE +except ValueError: + from core.exceptions import typeChecker, categoryChecker, UNSETVALUE + + +class Versions: + def __init__(self, layer="4.0", attack=UNSETVALUE, navigator=UNSETVALUE): + """ + Initialization - Creates a v4 Versions object + + :param layer: The layer version + :param attack: The attack version + :param navigator: The navigator version + """ + self.layer = layer + self.attack = attack + self.navigator = navigator + + @property + def attack(self): + return self.__attack + + @attack.setter + def attack(self, attack): + typeChecker(type(self).__name__, attack, str, "attack") + self.__attack = attack + + @property + def navigator(self): + return self.__navigator + + @navigator.setter + def navigator(self, navigator): + typeChecker(type(self).__name__, navigator, str, "navigator") + self.__navigator = navigator + + @property + def layer(self): + return self.__layer + + @layer.setter + def layer(self, layer): + typeChecker(type(self).__name__, layer, str, "layer") + categoryChecker(type(self).__name__, layer, ["3.0", "4.0"], "version") + self.__layer = layer + + def get_dict(self): + """ + Converts the currently loaded data into a dict + :returns: A dict representation of the local Versions object + """ + temp = dict() + listing = vars(self) + for entry in listing: + if listing[entry] != UNSETVALUE: + subname = entry.split('__')[-1] + temp[subname] = listing[entry] + return temp From cf8292cbecc0c6c793591c060537baac3f414313 Mon Sep 17 00:00:00 2001 From: clittle Date: Fri, 16 Oct 2020 11:18:55 -0400 Subject: [PATCH 3/7] Updated to full v4 spec, improved handling for load errors --- layers/core/exceptions.py | 20 +++++ layers/core/layer.py | 15 +--- layers/core/layerobj.py | 117 ++++++++++++++++-------------- layers/core/versions.py | 10 ++- layers/exporters/svg_templates.py | 9 +-- 5 files changed, 97 insertions(+), 74 deletions(-) diff --git a/layers/core/exceptions.py b/layers/core/exceptions.py index b3dfcc37..e68537d5 100644 --- a/layers/core/exceptions.py +++ b/layers/core/exceptions.py @@ -21,6 +21,9 @@ class UnknownTechniqueProperty(Exception): pass +class MissingParameters(Exception): + pass + def handler(caller, msg): """ Prints a debug/warning/error message @@ -85,3 +88,20 @@ def categoryChecker(caller, testee, valid, field): if testee not in valid: handler(caller, '{} not a valid value for {}'.format(testee, field)) raise BadInput + +def loadChecker(caller, testee, required, field): + """ + Verifies that the tested object contains all required fields + :param caller: the entity that called this function (used for error + messages) + :param testee: the element to test + :param requireds: a list of required values for the testee + :param field: what the element is to be used as (used for error + messages) + :raises BadInput: error denoting the testee element is not one of + the valid options + """ + for entry in required: + if entry not in testee: + handler(caller, '{} is not present in {} [{}]'.format(entry, field, testee)) + raise MissingParameters \ No newline at end of file diff --git a/layers/core/layer.py b/layers/core/layer.py index e7c226a9..e204e816 100644 --- a/layers/core/layer.py +++ b/layers/core/layer.py @@ -75,18 +75,7 @@ def _build(self): Loads the data stored in self.data into a LayerObj (self.layer) """ try: - if 'version' in self._data: - ver_obj = dict(layer=self._data['version']) - else: - ver_obj = self._data['versions'] - except: - handler(type(self).__name__, 'Layer version is malformed. ' - 'Unable to load') - self.__layer = None - return - try: - self.__layer = _LayerObj(ver_obj, self._data['name'], - self._data['domain']) + self.__layer = _LayerObj(self._data['name'], self._data['domain']) except BadType or BadInput as e: handler(type(self).__name__, 'Layer is malformed: {}. ' 'Unable to load.'.format(e)) @@ -99,7 +88,7 @@ def _build(self): return for key in self._data: - if key not in ['version', 'versions', 'name', 'domain']: + if key not in ['name', 'domain']: try: self.__layer._linker(key, self._data[key]) except Exception as e: diff --git a/layers/core/layerobj.py b/layers/core/layerobj.py index ae2f8824..9102cf24 100644 --- a/layers/core/layerobj.py +++ b/layers/core/layerobj.py @@ -7,7 +7,7 @@ from ..core.metadata import Metadata from ..core.versions import Versions from ..core.exceptions import UNSETVALUE, typeChecker, BadInput, handler, \ - categoryChecker, UnknownLayerProperty + categoryChecker, UnknownLayerProperty, loadChecker, MissingParameters except ValueError: from core.filter import Filter from core.layout import Layout @@ -17,19 +17,18 @@ from core.metadata import Metadata from core.versions import Versions from core.exceptions import UNSETVALUE, typeChecker, BadInput, handler, \ - categoryChecker, UnknownLayerProperty + categoryChecker, UnknownLayerProperty, loadChecker, MissingParameters class _LayerObj: - def __init__(self, versions, name, domain): + def __init__(self, name, domain): """ Initialization - Creates a layer object - :param version: The corresponding att&ck layer version :param name: The name for this layer - :param domain: The domain for this layer (mitre-enterprise - or mitre-mobile) + :param domain: The domain for this layer (enterprise-attack + or mobile-attack) """ - self.versions = versions + self.__versions = UNSETVALUE self.name = name self.__description = UNSETVALUE self.domain = domain @@ -48,28 +47,35 @@ def __init__(self, versions, name, domain): @property def version(self): - return self.__versions.layer + if self.__versions != UNSETVALUE: + return self.__versions.layer @version.setter def version(self, version): typeChecker(type(self).__name__, version, str, "version") categoryChecker(type(self).__name__, version, ["3.0", "4.0"], "version") + if self.__versions is UNSETVALUE: + self.__versions = Versions() self.__versions.layer = version @property def versions(self): - return self.__versions + if self.__versions != UNSETVALUE: + return self.__versions @versions.setter def versions(self, versions): typeChecker(type(self).__name__, versions, dict, "version") - attack = None - nav = None + attack = UNSETVALUE if 'attack' in versions: attack = versions['attack'] - if 'navigator' in versions: - nav = versions['navigator'] - self.__versions = Versions(versions['layer'], attack, nav) + try: + loadChecker(type(self).__name__, versions, ['layer', 'navigator'], "versions") + self.__versions = Versions(versions['layer'], attack, versions['navigator']) + except MissingParameters as e: + handler(type(self).__name__, 'versions {} is missing parameters: ' + '{}. Skipping.' + .format(versions, e)) @property def name(self): @@ -112,20 +118,16 @@ def filters(self): @filters.setter def filters(self, filters): - if self.version == "4.0": - temp = Filterv4(self.domain) - else: - temp = Filter(self.domain) + temp = Filterv4(self.domain) try: - if self.version != "4.0": - temp.stages = filters['stages'] + loadChecker(type(self).__name__, filters, ['platforms'], "filters") + # force upgrade to v4 temp.platforms = filters['platforms'] self.__filters = temp - except KeyError as e: - handler(type(self).__name__, "Unable to properly extract " - "information from filter: {}." - .format(e)) - raise BadInput + except MissingParameters as e: + handler(type(self).__name__, 'Filters {} is missing parameters: ' + '{}. Skipping.' + .format(filters, e)) @property def sorting(self): @@ -173,17 +175,17 @@ def techniques(self): def techniques(self, techniques): typeChecker(type(self).__name__, techniques, list, "techniques") self.__techniques = [] - entry = "" - try: - for entry in techniques: + + for entry in techniques: + try: + loadChecker(type(self).__name__, entry, ['techniqueID'], "technique") temp = Technique(entry['techniqueID']) temp._loader(entry) self.__techniques.append(temp) - except KeyError as e: - handler(type(self).__name__, "Unable to properly extract " - "information from technique {}: {}." - .format(entry, e)) - raise BadInput + except MissingParameters as e: + handler(type(self).__name__, 'Technique {} is missing parameters: ' + '{}. Skipping.' + .format(entry, e)) @property def gradient(self): @@ -193,12 +195,12 @@ def gradient(self): @gradient.setter def gradient(self, gradient): try: - self.__gradient = Gradient(gradient['colors'], - gradient['minValue'], - gradient['maxValue']) - except KeyError as e: - handler(type(self).__name__, 'Gradient is missing parameters: {}. ' - 'Unable to load.'.format(e)) + loadChecker(type(self).__name__, gradient, ['colors', 'minValue', 'maxValue'], "gradient") + self.__gradient = Gradient(gradient['colors'], gradient['minValue'], gradient['maxValue']) + except MissingParameters as e: + handler(type(self).__name__, 'Gradient {} is missing parameters: ' + '{}. Skipping.' + .format(gradient, e)) @property def legendItems(self): @@ -209,15 +211,15 @@ def legendItems(self): def legendItems(self, legendItems): typeChecker(type(self).__name__, legendItems, list, "legendItems") self.__legendItems = [] - entry = "" - try: - for entry in legendItems: + for entry in legendItems: + try: + loadChecker(type(self).__name__, entry, ['label', 'color'], "legendItem") temp = LegendItem(entry['label'], entry['color']) self.__legendItems.append(temp) - except KeyError as e: - handler(type(self).__name__, 'LegendItem {} is missing parameters:' - ' {}. Unable to load.' - .format(entry, e)) + except MissingParameters as e: + handler(type(self).__name__, 'Legend Item {} is missing parameters: ' + '{}. Skipping.' + .format(entry, e)) @property def showTacticRowBackground(self): @@ -272,13 +274,13 @@ def metadata(self): def metadata(self, metadata): typeChecker(type(self).__name__, metadata, list, "metadata") self.__metadata = [] - entry = "" - try: - for entry in metadata: + for entry in metadata: + try: + loadChecker(type(self).__name__, entry, ['name', 'value'], "metadata") self.__metadata.append(Metadata(entry['name'], entry['value'])) - except KeyError as e: - handler(type(self).__name__, 'Metadata {} is missing parameters: ' - '{}. Unable to load.' + except MissingParameters as e: + handler(type(self).__name__, 'Metadata {} is missing parameters: ' + '{}. Skipping.' .format(entry, e)) def _enumerate(self): @@ -321,10 +323,12 @@ def get_dict(self): Converts the currently loaded layer into a dict :returns: A dict representation of the current layer object """ - temp = dict(name=self.name, versions=self.versions.get_dict(), domain=self.domain) + temp = dict(name=self.name, domain=self.domain) if self.description: temp['description'] = self.description + if self.versions: + temp['versions'] = self.versions.get_dict() if self.filters: temp['filters'] = self.filters.get_dict() if self.sorting: @@ -364,6 +368,13 @@ def _linker(self, field, data): """ if field == 'description': self.description = data + elif field.startswith('version'): + if not field.endswith('s'): + # force upgrade + ver_obj = dict(layer="4.0", navigator="4.0") + self.versions = ver_obj + else: + self.versions = data elif field == 'filters': self.filters = data elif field == 'sorting': diff --git a/layers/core/versions.py b/layers/core/versions.py index a9f32670..75343b16 100644 --- a/layers/core/versions.py +++ b/layers/core/versions.py @@ -5,7 +5,7 @@ class Versions: - def __init__(self, layer="4.0", attack=UNSETVALUE, navigator=UNSETVALUE): + def __init__(self, layer="4.0", attack=UNSETVALUE, navigator="4.0"): """ Initialization - Creates a v4 Versions object @@ -14,7 +14,7 @@ def __init__(self, layer="4.0", attack=UNSETVALUE, navigator=UNSETVALUE): :param navigator: The navigator version """ self.layer = layer - self.attack = attack + self.__attack = attack self.navigator = navigator @property @@ -33,6 +33,7 @@ def navigator(self): @navigator.setter def navigator(self, navigator): typeChecker(type(self).__name__, navigator, str, "navigator") + categoryChecker(type(self).__name__, navigator, ["4.0"], "navigator version") self.__navigator = navigator @property @@ -42,7 +43,10 @@ def layer(self): @layer.setter def layer(self, layer): typeChecker(type(self).__name__, layer, str, "layer") - categoryChecker(type(self).__name__, layer, ["3.0", "4.0"], "version") + categoryChecker(type(self).__name__, layer, ["3.0", "4.0"], "layer version") + if layer == '3.0': + print('[NOTICE] - Forcibly upgrading version from {} to 4.0.'.format(layer)) + layer = "4.0" self.__layer = layer def get_dict(self): diff --git a/layers/exporters/svg_templates.py b/layers/exporters/svg_templates.py index 2ef69199..ee720ff7 100644 --- a/layers/exporters/svg_templates.py +++ b/layers/exporters/svg_templates.py @@ -8,13 +8,13 @@ from exporters.svg_objects import G, SVG_HeaderBlock, SVG_Technique, Text, convertToPx,_optimalFontSize, \ _getstringwidth from core.gradient import Gradient - from core.filter import Filter + from core.filter import Filterv4 except ModuleNotFoundError: from ..exporters.matrix_gen import MatrixGen from ..exporters.svg_objects import G, SVG_HeaderBlock, SVG_Technique, Text, convertToPx, _optimalFontSize, \ _getstringwidth from ..core.gradient import Gradient - from ..core.filter import Filter + from ..core.filter import Filterv4 class BadTemplateException(Exception): @@ -91,11 +91,10 @@ def _build_headers(self, name, config, desc=None, filters=None, gradient=None): if config.showFilters: fi = filters if fi is None: - fi = Filter() + fi = Filterv4() fi.platforms = ["Windows", "Linux", "macOS"] - fi.stages = ["act"] g2 = SVG_HeaderBlock().build(height=header_height, width=header_width, label='filters', - t1text=', '.join(fi.platforms), t2text=fi.stages[0], config=config) + t1text=', '.join(fi.platforms), config=config) b2 = G(tx=operation_x / header_count * psych + 1.5 * border * psych) header.append(b2) b2.append(g2) From 73eb254a7c1214a95547f37dc66732fa6dccc64c Mon Sep 17 00:00:00 2001 From: clittle Date: Fri, 16 Oct 2020 11:20:59 -0400 Subject: [PATCH 4/7] Updated Readme --- layers/README.md | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/layers/README.md b/layers/README.md index 8341682a..f8cffa43 100644 --- a/layers/README.md +++ b/layers/README.md @@ -5,14 +5,14 @@ This folder contains modules and scripts for working with ATT&CK Navigator layer #### Core Modules | script | description | |:-------|:------------| -| [filter](core/filter.py) | Implements a basic [filter object](https://github.com/mitre-attack/attack-navigator/blob/develop/layers/LAYERFORMATv3.md#filter-object-properties). | -| [gradient](core/gradient.py) | Implements a basic [gradient object](https://github.com/mitre-attack/attack-navigator/blob/develop/layers/LAYERFORMATv3.md#gradient-object-properties). | +| [filter](core/filter.py) | Implements a basic [filter object](https://github.com/mitre-attack/attack-navigator/blob/develop/layers/LAYERFORMATv4.md#filter-object-properties). | +| [gradient](core/gradient.py) | Implements a basic [gradient object](https://github.com/mitre-attack/attack-navigator/blob/develop/layers/LAYERFORMATv4.md#gradient-object-properties). | | [layer](core/layer.py) | Provides an interface for interacting with core module's layer representation. A further breakdown can be found in the corresponding [section](#Layer) below. | -| [layout](core/layout.py) | Implements a basic [layout object](https://github.com/mitre-attack/attack-navigator/blob/develop/layers/LAYERFORMATv3.md#layout-object-properties). | -| [legenditem](core/legenditem.py) | Implements a basic [legenditem object](https://github.com/mitre-attack/attack-navigator/blob/develop/layers/LAYERFORMATv3.md#legenditem-object-properties). | -| [metadata](core/metadata.py) | Implements a basic [metadata object](https://github.com/mitre-attack/attack-navigator/blob/develop/layers/LAYERFORMATv3.md#metadata-object-properties). | -| [technique](core/technique.py) | Implements a basic [technique object](https://github.com/mitre-attack/attack-navigator/blob/develop/layers/LAYERFORMATv3.md#technique-object-properties). | -| [version](core/versions.py) | Impelments a basic [version object]().| +| [layout](core/layout.py) | Implements a basic [layout object](https://github.com/mitre-attack/attack-navigator/blob/develop/layers/LAYERFORMATv4.md#layout-object-properties). | +| [legenditem](core/legenditem.py) | Implements a basic [legenditem object](https://github.com/mitre-attack/attack-navigator/blob/develop/layers/LAYERFORMATv4.md#legenditem-object-properties). | +| [metadata](core/metadata.py) | Implements a basic [metadata object](https://github.com/mitre-attack/attack-navigator/blob/develop/layers/LAYERFORMATv4.md#metadata-object-properties). | +| [technique](core/technique.py) | Implements a basic [technique object](https://github.com/mitre-attack/attack-navigator/blob/develop/layers/LAYERFORMATv4.md#technique-object-properties). | +| [versions](core/versions.py) | Impelments a basic [versions object](https://github.com/mitre-attack/attack-navigator/blob/develop/layers/LAYERFORMATv4.md#versions-object-properties).| #### Manipulator Scripts | script | description | |:-------|:------------| @@ -59,7 +59,8 @@ example_layer_dict = { example_layer4_dict = { "name": "layer v4 example", "versions" : { - "layer" : "4.0" + "layer" : "4.0", + "navigator": "4.0" }, "domain": "enterprise-attack" } From dfaa2d343f112bbc0570285272764c451aacaad0 Mon Sep 17 00:00:00 2001 From: clittle Date: Tue, 20 Oct 2020 11:47:41 -0400 Subject: [PATCH 5/7] Updated readme to explain v4 targeting --- layers/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/layers/README.md b/layers/README.md index f8cffa43..e9f74a93 100644 --- a/layers/README.md +++ b/layers/README.md @@ -1,6 +1,6 @@ # layers -This folder contains modules and scripts for working with ATT&CK Navigator layers. ATT&CK Navigator Layers are a set of annotations overlayed on top of the ATT&CK Matrix. For more about ATT&CK Navigator layers, visit the ATT&CK Navigator repository. The core module allows users to load, validate, manipulate, and save ATT&CK layers. A brief overview of the components can be found below. All scripts adhere to the MITRE ATT&CK Navigator Layer file format, [version 3.0](https://github.com/mitre-attack/attack-navigator/blob/develop/layers/LAYERFORMATv3.md). +This folder contains modules and scripts for working with ATT&CK Navigator layers. ATT&CK Navigator Layers are a set of annotations overlayed on top of the ATT&CK Matrix. For more about ATT&CK Navigator layers, visit the ATT&CK Navigator repository. The core module allows users to load, validate, manipulate, and save ATT&CK layers. A brief overview of the components can be found below. All scripts adhere to the MITRE ATT&CK Navigator Layer file format, [version 4.0](https://github.com/mitre-attack/attack-navigator/blob/develop/layers/LAYERFORMATv4.md), but will accept legacy [version 3.0](https://github.com/mitre-attack/attack-navigator/blob/develop/layers/LAYERFORMATv3.md) layers, upgrading them to version 4. #### Core Modules | script | description | @@ -50,7 +50,7 @@ The Layer class provides format validation and read/write capabilities to aid in #### Example Usage ```python -example_layer_dict = { +example_layer3_dict = { "name": "example layer", "version": "3.0", "domain": "mitre-enterprise" @@ -70,7 +70,7 @@ example_layer_out_location = "/path/to/new/layer/file.json" from layers.core import Layer -layer1 = Layer(example_layer_dict) # Create a new layer and load existing data +layer1 = Layer(example_layer3_dict) # Create a new layer and load existing data layer1.to_file(example_layer_out_location) # Write out the loaded layer to the specified file layer2 = Layer() # Create a new layer object From f8bbbfc964dfff04124b26a8ee67407630b9b13f Mon Sep 17 00:00:00 2001 From: clittle Date: Tue, 20 Oct 2020 11:57:08 -0400 Subject: [PATCH 6/7] Added warning flags for field upgrades --- layers/core/exceptions.py | 2 +- layers/core/layerobj.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/layers/core/exceptions.py b/layers/core/exceptions.py index e68537d5..bcfd2c34 100644 --- a/layers/core/exceptions.py +++ b/layers/core/exceptions.py @@ -104,4 +104,4 @@ def loadChecker(caller, testee, required, field): for entry in required: if entry not in testee: handler(caller, '{} is not present in {} [{}]'.format(entry, field, testee)) - raise MissingParameters \ No newline at end of file + raise MissingParameters diff --git a/layers/core/layerobj.py b/layers/core/layerobj.py index 9102cf24..f0a1b460 100644 --- a/layers/core/layerobj.py +++ b/layers/core/layerobj.py @@ -122,6 +122,8 @@ def filters(self, filters): try: loadChecker(type(self).__name__, filters, ['platforms'], "filters") # force upgrade to v4 + if 'stages' in filters: + print('[Filters] - V3 Field "stages" detected. Upgrading Filters object to V4.') temp.platforms = filters['platforms'] self.__filters = temp except MissingParameters as e: @@ -371,6 +373,7 @@ def _linker(self, field, data): elif field.startswith('version'): if not field.endswith('s'): # force upgrade + print('[Version] - V3 version field detected. Upgrading to V4 Versions object.') ver_obj = dict(layer="4.0", navigator="4.0") self.versions = ver_obj else: From b1834b4c0b405a9c63af1bc103a12887d483c767 Mon Sep 17 00:00:00 2001 From: clittle Date: Wed, 21 Oct 2020 09:30:13 -0400 Subject: [PATCH 7/7] Address minor style issues, old default value --- layers/core/filter.py | 6 +++--- layers/core/layerobj.py | 4 ++-- layers/exporters/svg_templates.py | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/layers/core/filter.py b/layers/core/filter.py index d99973e7..ad59e0a8 100644 --- a/layers/core/filter.py +++ b/layers/core/filter.py @@ -6,8 +6,8 @@ UNSETVALUE -class Filterv4: - def __init__(self, domain="mitre-enterprise"): +class Filter: + def __init__(self, domain="enterprise-attack"): """ Initialization - Creates a filter object, with an optional domain input @@ -52,7 +52,7 @@ def get_dict(self): if len(temp) > 0: return temp -class Filter(Filterv4): +class Filterv3(Filter): def __init__(self, domain="mitre-enterprise"): self.__stages = UNSETVALUE super().__init__(domain) diff --git a/layers/core/layerobj.py b/layers/core/layerobj.py index f0a1b460..d92b80f1 100644 --- a/layers/core/layerobj.py +++ b/layers/core/layerobj.py @@ -1,5 +1,5 @@ try: - from ..core.filter import Filter, Filterv4 + from ..core.filter import Filter from ..core.layout import Layout from ..core.technique import Technique from ..core.gradient import Gradient @@ -118,7 +118,7 @@ def filters(self): @filters.setter def filters(self, filters): - temp = Filterv4(self.domain) + temp = Filter(self.domain) try: loadChecker(type(self).__name__, filters, ['platforms'], "filters") # force upgrade to v4 diff --git a/layers/exporters/svg_templates.py b/layers/exporters/svg_templates.py index ee720ff7..43a657c8 100644 --- a/layers/exporters/svg_templates.py +++ b/layers/exporters/svg_templates.py @@ -8,13 +8,13 @@ from exporters.svg_objects import G, SVG_HeaderBlock, SVG_Technique, Text, convertToPx,_optimalFontSize, \ _getstringwidth from core.gradient import Gradient - from core.filter import Filterv4 + from core.filter import Filter except ModuleNotFoundError: from ..exporters.matrix_gen import MatrixGen from ..exporters.svg_objects import G, SVG_HeaderBlock, SVG_Technique, Text, convertToPx, _optimalFontSize, \ _getstringwidth from ..core.gradient import Gradient - from ..core.filter import Filterv4 + from ..core.filter import Filter class BadTemplateException(Exception): @@ -91,7 +91,7 @@ def _build_headers(self, name, config, desc=None, filters=None, gradient=None): if config.showFilters: fi = filters if fi is None: - fi = Filterv4() + fi = Filter() fi.platforms = ["Windows", "Linux", "macOS"] g2 = SVG_HeaderBlock().build(height=header_height, width=header_width, label='filters', t1text=', '.join(fi.platforms), config=config)