diff --git a/layers/README.md b/layers/README.md index ef5e92a5..e9f74a93 100644 --- a/layers/README.md +++ b/layers/README.md @@ -1,18 +1,18 @@ # 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 | |:-------|:------------| -| [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). | - +| [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 | |:-------|:------------| @@ -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 | |:-------|:------------| @@ -50,22 +50,31 @@ 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" } +example_layer4_dict = { + "name": "layer v4 example", + "versions" : { + "layer" : "4.0", + "navigator": "4.0" + }, + "domain": "enterprise-attack" +} + example_layer_location = "/path/to/layer/file.json" 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 -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/exceptions.py b/layers/core/exceptions.py index b3dfcc37..bcfd2c34 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 diff --git a/layers/core/filter.py b/layers/core/filter.py index 5b36a8aa..ad59e0a8 100644 --- a/layers/core/filter.py +++ b/layers/core/filter.py @@ -7,7 +7,7 @@ class Filter: - def __init__(self, domain="mitre-enterprise"): + def __init__(self, domain="enterprise-attack"): """ Initialization - Creates a filter object, with an optional domain input @@ -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: @@ -59,7 +46,25 @@ 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 + +class Filterv3(Filter): + 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/layer.py b/layers/core/layer.py index cb17a099..e204e816 100644 --- a/layers/core/layer.py +++ b/layers/core/layer.py @@ -75,8 +75,7 @@ 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'], - 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)) @@ -89,7 +88,7 @@ def _build(self): return for key in self._data: - if key not in ['version', '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 dda4a6a5..d92b80f1 100644 --- a/layers/core/layerobj.py +++ b/layers/core/layerobj.py @@ -5,8 +5,9 @@ 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 + categoryChecker, UnknownLayerProperty, loadChecker, MissingParameters except ValueError: from core.filter import Filter from core.layout import Layout @@ -14,20 +15,20 @@ 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 + categoryChecker, UnknownLayerProperty, loadChecker, MissingParameters class _LayerObj: - def __init__(self, version, 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.version = version + self.__versions = UNSETVALUE self.name = name self.__description = UNSETVALUE self.domain = domain @@ -46,13 +47,35 @@ def __init__(self, version, name, domain): @property def version(self): - return self.__version + 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"], "version") - self.__version = 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): + if self.__versions != UNSETVALUE: + return self.__versions + + @versions.setter + def versions(self, versions): + typeChecker(type(self).__name__, versions, dict, "version") + attack = UNSETVALUE + if 'attack' in versions: + attack = versions['attack'] + 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): @@ -70,8 +93,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 @@ -92,16 +118,18 @@ def filters(self): @filters.setter def filters(self, filters): + temp = Filter(self.domain) try: - temp = Filter(self.domain) - temp.stages = filters['stages'] + 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 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): @@ -149,17 +177,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): @@ -169,12 +197,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): @@ -185,15 +213,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): @@ -248,13 +276,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): @@ -297,10 +325,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, version=self.version, 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: @@ -340,6 +370,14 @@ def _linker(self, field, data): """ if field == 'description': self.description = 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: + self.versions = data elif field == 'filters': self.filters = data elif field == 'sorting': diff --git a/layers/core/versions.py b/layers/core/versions.py new file mode 100644 index 00000000..75343b16 --- /dev/null +++ b/layers/core/versions.py @@ -0,0 +1,63 @@ +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="4.0"): + """ + 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") + categoryChecker(type(self).__name__, navigator, ["4.0"], "navigator version") + 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"], "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): + """ + 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 diff --git a/layers/exporters/svg_templates.py b/layers/exporters/svg_templates.py index 2ef69199..43a657c8 100644 --- a/layers/exporters/svg_templates.py +++ b/layers/exporters/svg_templates.py @@ -93,9 +93,8 @@ def _build_headers(self, name, config, desc=None, filters=None, gradient=None): if fi is None: fi = Filter() 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)