diff --git a/CHANGELOG.md b/CHANGELOG.md index 20963740..560a8a15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +# v1.9.1 - 9 April 2021 +With the release of our [mitreattack-python](https://github.com/mitre-attack/mitreattack-python) pip module, we're removing the tools previously found in the `layers` folder as they are now part of that module. + +## Improvements +- Updated documentation to highlight the new [mitreattack-python](https://github.com/mitre-attack/mitreattack-python) pip module. +- Removed the scripts in the `layers` folder which are now part of [mitreattack-python](https://github.com/mitre-attack/mitreattack-python). +- Cleaned up `requirements.txt` to remove modules that are no longer required. + + # v1.9.0 - 23 March 2021 ## Improvements - [diff_stix.py](scripts/diff_stix.py) now supports an `--unchanged` argument which adds a section listing objects which did _not_ change between releases. diff --git a/README.md b/README.md index 803ac1e6..ee4148c2 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,10 @@ # attack-scripts -This repository contains various tools and utilities for working with ATT&CK content. +This repository contains standalone scripts and utilities for working with ATT&CK. - the [scripts](scripts) folder contains one-off scripts for processing and visualizing ATT&CK content. - the [scripts/layers/samples](scripts/layers/samples) folder contains one-off scripts demonstrating the generation of ATT&CK Navigator layers from ATT&CK data. The outputs of these scripts can also be found on the [ATT&CK Navigator repository](https://github.com/mitre-attack/attack-navigator/tree/master/layers/data/samples). -- the [layers](layers) folder contains a collection of modules and scripts for working with [ATT&CK Navigator](https://github.com/mitre-attack/attack-navigator) layers. + +See also our [mitreattack-python](https://github.com/mitre-attack/mitreattack-python/) pip module for more python tools! ## Requirements - [python3](https://www.python.org/) diff --git a/layers/README.md b/layers/README.md index 76daf673..c87d0cb3 100644 --- a/layers/README.md +++ b/layers/README.md @@ -1,248 +1,3 @@ # layers -This folder contains modules and scripts for working with ATT&CK Navigator layers. ATT&CK Navigator Layers are a set of annotations overlaid 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.1](https://github.com/mitre-attack/attack-navigator/blob/develop/layers/LAYERFORMATv4_1.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.1. - -#### Core Modules -| script | description | -|:-------|:------------| -| [filter](core/filter.py) | Implements a basic [filter object](https://github.com/mitre-attack/attack-navigator/blob/develop/layers/LAYERFORMATv4_1.md#filter-object-properties). | -| [gradient](core/gradient.py) | Implements a basic [gradient object](https://github.com/mitre-attack/attack-navigator/blob/develop/layers/LAYERFORMATv4_1.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/LAYERFORMATv4_1.md#layout-object-properties). | -| [legenditem](core/legenditem.py) | Implements a basic [legenditem object](https://github.com/mitre-attack/attack-navigator/blob/develop/layers/LAYERFORMATv4_1.md#legenditem-object-properties). | -| [metadata](core/metadata.py) | Implements a basic [metadata object](https://github.com/mitre-attack/attack-navigator/blob/develop/layers/LAYERFORMATv4_1.md#metadata-object-properties). | -| [technique](core/technique.py) | Implements a basic [technique object](https://github.com/mitre-attack/attack-navigator/blob/develop/layers/LAYERFORMATv4_1.md#technique-object-properties). | -| [versions](core/versions.py) | Impelments a basic [versions object](https://github.com/mitre-attack/attack-navigator/blob/develop/layers/LAYERFORMATv4_1.md#versions-object-properties).| -#### Manipulator Scripts -| script | description | -|:-------|:------------| -| [layerops](manipulators/layerops.py) | Provides a means by which to combine multiple ATT&CK layer objects in customized ways. A further breakdown can be found in the corresponding [section](#layerops.py) below. | - -#### Exporter Scripts -| script | description | -|:-------|:------------| -| [to_excel](exporters/to_excel.py) | Provides a means by which to export an ATT&CK Layer to an excel file. A further breakdown can be found in the corresponding [section](#to_excel.py) below. | -| [to_svg](exporters/to_svg.py) | Provides a means by which to export an ATT&CK layer to an svg image file. A further breakdown can be found in the corresponding [section](#to_svg.py) below. This file also contains the `SVGConfig` object that can be used to configure the SVG export.| -##### Utility Modules -| script | description | -|:-------|:------------| -| [excel_templates](exporters/excel_templates.py) | Provides a means by which to convert a matrix into a clean excel matrix template. | -| [matrix_gen](exporters/matrix_gen.py) | Provides a means by which to generate a matrix from raw data, either from the ATT&CK TAXII server or from a local STIX Bundle. | -| [svg_templates](exporters/svg_templates.py) | Provides a means by which to convert a layer file into a marked up svg file. | -| [svg_objects](exporters/svg_objects.py) | Provides raw templates and supporting functionality for generating svg objects. | -##### Command Line Tools -| script | description | -|:-------|:------------| -| [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 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 | -|:-------|:------------| -| x.from_str(_input_) | Loads an ATT&CK layer from a string representation of a json layer. | -| x.from_dict(_input_) | Loads an ATT&CK layer from a dictionary. | -| x.from_file(_filepath_) | Loads an ATT&CK layer from a file location specified by the _filepath_. | -| x.to_file(_filepath_) | Saves the current state of the loaded ATT&CK layer to a json file denoted by the _filepath_. | -| x.to_dict() | Returns a representation of the current ATT&CK layer object as a dictionary. | -| x.to_str() | Returns a representation of the current ATT&CK layer object as a string representation of a dictionary. | - -#### Example Usage - -```python -example_layer3_dict = { - "name": "example layer", - "version": "3.0", - "domain": "mitre-enterprise" -} - -example_layer4_dict = { - "name": "layer v4.1 example", - "versions" : { - "attack": "8", - "layer" : "4.1", - "navigator": "4.1" - }, - "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_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_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 -layer3.from_file(example_layer_location) # Load layer data from a file into existing layer object -``` - -## layerops.py -Layerops.py provides the LayerOps class, which is a way to combine layer files in an automated way, using user defined lambda functions. Each LayerOps instance, when created, ingests the provided lambda functions, and stores them for use. An existing LayerOps class can be used to combine layer files according to the initialized lambda using the process method. The breakdown of this two step process is documented in the table below, while examples of both the list and dictionary modes of operation can be found below. - -##### LayerOps() -```python - x = LayerOps(score=score, comment=comment, enabled=enabled, colors=colors, metadata=metadata, name=name, desc=desc, default_values=default_values) -``` - - Each of the _inputs_ takes a lambda function that will be used to combine technique object fields matching the parameter. The one exception to this is _default_values_, which is an optional dictionary argument containing default values to provide the lambda functions if techniques of the combined layers are missing them. - -##### .process() Method -```python -x.process(data, default_values=default_values) -``` -The process method applies the lambda functions stored during initialization to the layer objects in _data_. _data_ must be either a list or a dictionary of Layer objects, and is expected to match the format of the lambda equations provided during initialization. default_values is an optional dictionary argument that overrides the currently stored default - values with new ones for this specific processing operation. - -#### Example Usage -```python -from layers.manipulators.layerops import LayerOps -from layers.core.layer import Layer - -demo = Layer() -demo.from_file("C:\Users\attack\Downloads\layer.json") -demo2 = Layer() -demo2.from_file("C:\Users\attack\Downloads\layer2.json") -demo3 = Layer() -demo3.from_file("C:\Users\attack\Downloads\layer3.json") - -# Example 1) Build a LayerOps object that takes a list and averages scores across the layers -lo = LayerOps(score=lambda x: sum(x) / len(x), - name=lambda x: x[1], - desc=lambda x: "This is an list example") # Build LayerOps object -out_layer = lo.process([demo, demo2]) # Trigger processing on a list of demo and demo2 layers -out_layer.to_file("C:\demo_layer1.json") # Save averaged layer to file -out_layer2 = lo.process([demo, demo2, demo3]) # Trigger processing on a list of demo, demo2, demo3 -visual_aid = out_layer2.to_dict() # Retrieve dictionary representation of processed layer - -# Example 2) Build a LayerOps object that takes a dictionary and averages scores across the layers -lo2 = LayerOps(score=lambda x: sum([x[y] for y in x]) / len([x[y] for y in x]), - color=lambda x: x['b'], - desc=lambda x: "This is a dict example") # Build LayerOps object, with lambda -out_layer3 = lo2.process({'a': demo, 'b': demo2}) # Trigger processing on a dictionary of demo and demo2 -dict_layer = out_layer3.to_dict() # Retrieve dictionary representation of processed layer -print(dict_layer) # Display retrieved dictionary -out_layer4 = lo2.process({'a': demo, 'b': demo2, 'c': demo3})# Trigger processing on a dictionary of demo, demo2, demo3 -out_layer4.to_file("C:\demo_layer4.json") # Save averaged layer to file - -# Example 3) Build a LayerOps object that takes a single element dictionary and inverts the score -lo3 = LayerOps(score=lambda x: 100 - x['a'], - desc= lambda x: "This is a simple example") # Build LayerOps object to invert score (0-100 scale) -out_layer5 = lo3.process({'a': demo}) # Trigger processing on dictionary of demo -print(out_layer5.to_dict()) # Display processed layer in dictionary form -out_layer5.to_file("C:\demo_layer5.json") # Save inverted score layer to file - -# Example 4) Build a LayerOps object that combines the comments from elements in the list, with custom defaults -lo4 = LayerOps(score=lambda x: '; '.join(x), - default_values= { - "comment": "This was an example of new default values" - }, - desc= lambda x: "This is a defaults example") # Build LayerOps object to combine descriptions, defaults -out_layer6 = lo4.process([demo2, demo3]) # Trigger processing on a list of demo2 and demo0 -out_layer6.to_file("C:\demo_layer6.json") # Save combined comment layer to file -``` - -## to_excel.py -to_excel.py provides the ToExcel class, which is a way to export an existing layer file as an Excel -spreadsheet. The ToExcel class has an optional parameter for the initialization function, that -tells the exporter what data source to use when building the output matrix. Valid options include using live data from cti-taxii.mitre.org or using a local STIX bundle. - -##### ToExcel() -```python -x = ToExcel(domain='enterprise', source='taxii', local=None) -``` -The ToExcel constructor takes domain, server, and local arguments during instantiation. The domain can -be either `enterprise` or `mobile`, and can be pulled directly from a layer file as `layer.domain`. The source argument tells the matrix generation tool which data source to use when building the matrix. `taxii` indicates that the tool should utilize the official ATT&CK Taxii Server (`cti-taxii`) when building the matrix, while the `local` option indicates that it should use a local bundle respectively. The local argument is only required if the source is set to `local`, in which case it should be a path to a local stix bundle. - -##### .to_xlsx() Method -```python -x.to_xlsx(layer=layer, filepath="layer.xlsx") -``` -The to_xlsx method exports the layer file referenced as `layer`, as an excel file to the -`filepath` specified. - -#### Example Usage -```python -from layers import Layer -from layers import ToExcel - -lay = Layer() -lay.from_file("path/to/layer/file.json") -# Using taxii server for template -t = ToExcel(domain=lay.layer.domain, source='taxii') -t.to_xlsx(layer=lay, filepath="demo.xlsx") -#Using local stix data for template -t2 = ToExcel(domain='mobile', source='local', local='path/to/local/stix.json') -t2.to_xlsx(layer=lay, filepath="demo2.xlsx") -``` - -## to_svg.py -to_svg.py provides the ToSvg class, which is a way to export an existing layer file as an SVG image file. The ToSvg class, like the ToExcel class, has an optional parameter for the initialization function, that -tells the exporter what data source to use when building the output matrix. Valid options include using live data from cti-taxii.mitre.org or using a local STIX bundle. - -##### ToSvg() -```python -x = ToSvg(domain='enterprise', source='taxii', local=None, config=None) -``` -The ToSvg constructor, just like the ToExcel constructor, takes domain, server, and local arguments during instantiation. The domain can be either `enterprise` or `mobile`, and can be pulled directly from a layer file as `layer.domain`. The source argument tells the matrix generation tool which data source to use when building the matrix. `taxii` indicates that the tool should utilize the `cti-taxii` server when building the matrix, while the `local` option indicates that it should use a local bundle respectively. The local argument is only required if the source is set to `local`, in which case it should be a path to a local stix bundle. The `config` parameter is an optional SVGConfig object that can be used to configure the export as desired. If not provided, the configuration for the export will be set to default values. - -##### SVGConfig() -```python -y = SVGConfig(width=8.5, height=11, headerHeight=1, unit="in", showSubtechniques="expanded", - font="sans-serif", tableBorderColor="#6B7279", showHeader=True, legendDocked=True, - legendX=0, legendY=0, legendWidth=2, legendHeight=1, showLegend=True, showFilters=True, - showAbout=True, showDomain=True, border=0.104) -``` -The SVGConfig object is used to configure how an SVG export behaves. The defaults for each of the available values can be found in the declaration above, and a brief explanation for each field is included in the table below. The config object should be provided to the ToSvg object during instantiation, but if values need to be updated on the fly, the currently loaded configuration can be interacted with at `ToSvg().config`. The configuration can also be populated from a json file using the `.load_from_file(filename="path/to/file.json")` method, or stored to one using the `.save_to_file(filename="path/to/file.json)` method. - -| attribute| description | type | default value | -|:-------|:------------|:------------|:------------| -| width | Desired SVG width | number | 8.5 | -| height | Desired SVG height | number | 11 | -| headerHeight | Desired Header Block height | number | 1 | -| unit | SVG measurement units (qualifies width, height, etc.) - "in", "cm", "px", "em", or "pt"| string | "in" | -| showSubtechniques | Display form for subtechniques - "all", "expanded" (decided by layer), or "none" | string | "expanded" | -| font | What font style to use - "serif", "sans-serif", or "monospace" | string | "sans-serif" | -| tableBorderColor | Hex color to use for the technique borders | string | "#6B7279" | -| showHeader | Whether or not to show Header Blocks | bool | True | -| legendDocked | Whether or not the legend should be docked | bool | True | -| legendX | Where to place the legend on the x axis if not docked | number | 0 | -| legendY | Where to place the legend on the y axis if not docked | number | 1 | -| legendWidth | Width of the legend if not docked | number | 2 | -| legendHeight | Height of the legend if not docked | number | 1 | -| showLegend | Whether or not to show the legend | bool | True | -| showFilters | Whether or not to show the Filter Header Block | bool | True | -| showDomain | Whether or not to show the Domain and Version Header Block | bool | True | -| showAbout | Whether or not to show the About Header Block | bool | True | -| border | What default border width to use | number | 0.104 | - -##### .to_svg() Method -```python -x.to_svg(layer=layer, filepath="layer.svg") -``` -The to_svg method exports the layer file referenced as `layer`, as an excel file to the -`filepath` specified. - -#### Example Usage -```python -from layers import Layer -from layers import ToSvg, SVGConfig - -lay = Layer() -lay.from_file("path/to/layer/file.json") -# Using taxii server for template -t = ToSvg(domain=lay.layer.domain, source='taxii') -t.to_svg(layer=lay, filepath="demo.svg") -#Using local stix data for template - -conf = SVGConfig() -conf.load_from_file(filename="path/to/poster/config.json") - -t2 = ToSvg(domain='mobile', source='local', local='path/to/local/stix.json', config=conf) -t2.to_svg(layer=lay, filepath="demo2.svg") -``` +This folder previously contained tools for working with ATT&CK Navigator layers. These tools have been moved to our [mitreattack-python](https://github.com/mitre-attack/mitreattack-python/) pip module. \ No newline at end of file diff --git a/layers/__init__.py b/layers/__init__.py deleted file mode 100644 index 7ac15a2b..00000000 --- a/layers/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .core import * -from .exporters import * -from .manipulators import * \ No newline at end of file diff --git a/layers/core/__init__.py b/layers/core/__init__.py deleted file mode 100644 index b6b4ae3e..00000000 --- a/layers/core/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -from .layer import Layer -from .exceptions import * -from .filter import Filter -from .gradient import Gradient -from .layerobj import _LayerObj -from .layout import Layout -from .legenditem import LegendItem -from .metadata import Metadata -from .technique import Technique - diff --git a/layers/core/exceptions.py b/layers/core/exceptions.py deleted file mode 100644 index bcfd2c34..00000000 --- a/layers/core/exceptions.py +++ /dev/null @@ -1,107 +0,0 @@ -UNSETVALUE = '(x)' - - -class BadInput(Exception): - pass - - -class BadType(Exception): - pass - - -class UninitializedLayer(Exception): - pass - - -class UnknownLayerProperty(Exception): - pass - - -class UnknownTechniqueProperty(Exception): - pass - - -class MissingParameters(Exception): - pass - -def handler(caller, msg): - """ - Prints a debug/warning/error message - :param caller: the entity that called this function - :param msg: the message to log - """ - print('[{}] - {}'.format(caller, msg)) - - -def typeChecker(caller, testee, type, field): - """ - Verifies that the tested object is of the correct type - :param caller: the entity that called this function (used for error - messages) - :param testee: the element to test - :param type: the type the element should be - :param field: what the element is to be used as (used for error - messages) - :raises BadType: error denoting the testee element is not of the - correct type - """ - if not isinstance(testee, type): - handler(caller, '{} [{}] is not a {}'.format(testee, field, - str(type))) - raise BadType - - -def typeCheckerArray(caller, testee, type, field): - """ - Verifies that the tested object is an array of the correct type - :param caller: the entity that called this function (used for error - messages) - :param testee: the element to test - :param type: the type the element should be - :param field: what the element is to be used as (used for error - messages) - :raises BadType: error denoting the testee element is not of the - correct type - """ - if not isinstance(testee, list): - handler(caller, '{} [{}] is not a {}'.format(testee, field, - "Array")) - raise BadType - if not isinstance(testee[0], type): - handler(caller, '{} [{}] is not a {}'.format(testee, field, - "Array of " + type)) - raise BadType - - -def categoryChecker(caller, testee, valid, field): - """ - Verifies that the tested object is one of a set of valid values - :param caller: the entity that called this function (used for error - messages) - :param testee: the element to test - :param valid: a list of valid 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 - """ - 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 deleted file mode 100644 index 43fab1f7..00000000 --- a/layers/core/filter.py +++ /dev/null @@ -1,65 +0,0 @@ -try: - from ..core.exceptions import typeCheckerArray, categoryChecker, \ - UNSETVALUE -except ValueError: - from core.exceptions import typeCheckerArray, categoryChecker, \ - UNSETVALUE - - -class Filter: - def __init__(self, domain="enterprise-attack"): - """ - Initialization - Creates a filter object, with an optional - domain input - - :param domain: The domain used for this layer (mitre-enterprise - or mitre-mobile) - """ - self.domain = domain - self.__platforms = UNSETVALUE - - @property - def platforms(self): - if self.__platforms != UNSETVALUE: - return self.__platforms - - @platforms.setter - def platforms(self, platforms): - typeCheckerArray(type(self).__name__, platforms, str, "platforms") - self.__platforms = [] - for entry in platforms: - self.__platforms.append(entry) - - def get_dict(self): - """ - Converts the currently loaded data into a dict - :returns: A dict representation of the local filter object - """ - temp = dict() - listing = vars(self) - for entry in listing: - if entry == 'domain': - continue - if listing[entry] != UNSETVALUE: - 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/gradient.py b/layers/core/gradient.py deleted file mode 100644 index b2858814..00000000 --- a/layers/core/gradient.py +++ /dev/null @@ -1,97 +0,0 @@ -import colour -import math -try: - from ..core.exceptions import typeChecker, typeCheckerArray -except ValueError: - from core.exceptions import typeChecker, typeCheckerArray - - -class Gradient: - def __init__(self, colors, minValue, maxValue): - """ - Initialization - Creates a gradient object - - :param colors: The array of color codes for this gradient - :param minValue: The minValue for this gradient - :param maxValue: The maxValue for this gradient - """ - self.__minValue = None - self.__maxValue = None - self.colors = colors - self.minValue = minValue - self.maxValue = maxValue - - @property - def colors(self): - return self.__colors - - @colors.setter - def colors(self, colors): - typeCheckerArray(type(self).__name__, colors, str, "colors") - self.__colors = [] - for entry in colors: - self.__colors.append(entry) - self._compute_curve() - - @property - def minValue(self): - return self.__minValue - - @minValue.setter - def minValue(self, minValue): - typeChecker(type(self).__name__, minValue, int, "minValue") - self.__minValue = minValue - self._compute_curve() - - @property - def maxValue(self): - return self.__maxValue - - @maxValue.setter - def maxValue(self, maxValue): - typeChecker(type(self).__name__, maxValue, int, "maxValue") - self.__maxValue = maxValue - self._compute_curve() - - def _compute_curve(self): - """ - Computes the gradient color curve - """ - if self.maxValue is not None and self.minValue is not None and self.colors is not None: - chunksize = int(math.floor((self.maxValue - self.minValue)/(len(self.colors) - 1))) - fchunksize = int(math.ceil((self.maxValue - self.minValue)/(len(self.colors) - 1))) - self.curve = [] - index = 1 - while index < len(self.colors): - s_c = colour.Color(self.colors[index-1]) - e_c = colour.Color(self.colors[index]) - if index == len(self.colors): - curve_2 = list(s_c.range_to(e_c, fchunksize)) - else: - curve_2 = list(s_c.range_to(e_c, chunksize)) - index += 1 - self.curve.extend(curve_2) - self.curve.append(colour.Color(self.colors[-1])) - - def compute_color(self, score): - """ - Computes a specific color based on the score value provided - :returns: A hexadecimal color representation of the score on - the gradient - """ - if score <= self.minValue: - return self.curve[0].hex_l - if score >= self.maxValue: - return self.curve[-1].hex_l - - target = self.curve[score - self.minValue] - return target.hex_l - - - def get_dict(self): - """ - Converts the currently loaded gradient file into a dict - :returns: A dict representation of the current gradient object - """ - return dict(colors=self.__colors, minValue=self.__minValue, - maxValue=self.maxValue) diff --git a/layers/core/layer.py b/layers/core/layer.py deleted file mode 100644 index e204e816..00000000 --- a/layers/core/layer.py +++ /dev/null @@ -1,117 +0,0 @@ -import json -try: - from ..core.exceptions import UninitializedLayer, BadType, BadInput, \ - handler - from ..core.layerobj import _LayerObj -except ValueError: - from core.exceptions import UninitializedLayer, BadType, BadInput, \ - handler - from core.layerobj import _LayerObj - - -class Layer: - def __init__(self, init_data={}, strict=True): - """ - Initialization - create a new Layer object - :param init_data: Optionally provide base Layer json or string - data on initialization - """ - self.__layer = None - self.strict = strict - if isinstance(init_data, str): - self.from_str(init_data) - else: - self.from_dict(init_data) - - @property - def layer(self): - if self.__layer is not None: - return self.__layer - return "No Layer Loaded Yet!" - - def from_str(self, init_str): - """ - Loads a raw layer string into the object - :param init_str: the string representing the layer data to - be loaded - """ - self._data = json.loads(init_str) - self._build() - - def from_dict(self, init_dict): - """ - Loads a raw layer string into the object - :param init_dict: the dictionary representing the layer data to - be loaded - """ - self._data = init_dict - if self._data != {}: - self._build() - - def from_file(self, filename): - """ - loads input from a layer file specified by filename - :param filename: the target filename to load from - """ - with open(filename, 'r') as fio: - raw = fio.read() - self._data = json.loads(raw) - self._build() - - def to_file(self, filename): - """ - saves the current state of the layer to a layer file specified by - filename - :param filename: the target filename to save as - """ - if self.__layer is not None: - with open(filename, 'w') as fio: - json.dump(self.__layer.get_dict(), fio) - else: - raise UninitializedLayer - - def _build(self): - """ - Loads the data stored in self.data into a LayerObj (self.layer) - """ - try: - 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)) - self.__layer = None - return - except KeyError as e: - handler(type(self).__name__, 'Layer is missing parameters: {}. ' - 'Unable to load.'.format(e)) - self.__layer = None - return - - for key in self._data: - if key not in ['name', 'domain']: - try: - self.__layer._linker(key, self._data[key]) - except Exception as e: - if self.strict: - handler(type(self).__name__, "{} error. " - "Unable to load." - .format(str(e.__class__.__name__))) - self.__layer = None - return - - def to_dict(self): - """ - Converts the currently loaded layer file into a dict - :returns: A dict representation of the current layer object - """ - if self.__layer is not None: - return self.__layer.get_dict() - - def to_str(self): - """ - Converts the currently loaded layer file into a string - representation of a dictionary - :returns: A string representation of the current layer object - """ - if self.__layer is not None: - return json.dumps(self.to_dict()) diff --git a/layers/core/layerobj.py b/layers/core/layerobj.py deleted file mode 100644 index 7f996675..00000000 --- a/layers/core/layerobj.py +++ /dev/null @@ -1,411 +0,0 @@ -try: - from ..core.filter import Filter - from ..core.layout import Layout - from ..core.technique import Technique - from ..core.gradient import Gradient - from ..core.legenditem import LegendItem - from ..core.metadata import Metadata, MetaDiv - from ..core.versions import Versions - from ..core.exceptions import UNSETVALUE, typeChecker, BadInput, handler, \ - categoryChecker, UnknownLayerProperty, loadChecker, MissingParameters -except ValueError: - from core.filter import Filter - from core.layout import Layout - from core.technique import Technique - from core.gradient import Gradient - from core.legenditem import LegendItem - from core.metadata import Metadata, MetaDiv - from core.versions import Versions - from core.exceptions import UNSETVALUE, typeChecker, BadInput, handler, \ - categoryChecker, UnknownLayerProperty, loadChecker, MissingParameters - -class _LayerObj: - def __init__(self, name, domain): - """ - Initialization - Creates a layer object - - :param name: The name for this layer - :param domain: The domain for this layer (enterprise-attack - or mobile-attack) - """ - self.__versions = UNSETVALUE - self.name = name - self.__description = UNSETVALUE - self.domain = domain - self.__filters = UNSETVALUE - self.__sorting = UNSETVALUE - self.__layout = UNSETVALUE - self.__hideDisabled = UNSETVALUE - self.__techniques = UNSETVALUE - self.__gradient = UNSETVALUE - self.__legendItems = UNSETVALUE - self.__showTacticRowBackground = UNSETVALUE - self.__tacticRowBackground = UNSETVALUE - self.__selectTechniquesAcrossTactics = UNSETVALUE - self.__selectSubtechniquesWithParent = UNSETVALUE - self.__metadata = UNSETVALUE - - @property - def version(self): - 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", "4.1"], "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): - return self.__name - - @name.setter - def name(self, name): - typeChecker(type(self).__name__, name, str, "name") - self.__name = name - - @property - def domain(self): - return self.__domain - - @domain.setter - def domain(self, domain): - typeChecker(type(self).__name__, domain, str, "domain") - dom = domain - if dom.startswith('mitre'): - dom = dom.split('-')[-1] + '-attack' - categoryChecker(type(self).__name__, dom, ["enterprise-attack", - "mobile-attack"], - "domain") - self.__domain = domain - - @property - def description(self): - if self.__description != UNSETVALUE: - return self.__description - - @description.setter - def description(self, description): - typeChecker(type(self).__name__, description, str, "description") - self.__description = description - - @property - def filters(self): - if self.__filters != UNSETVALUE: - return self.__filters - - @filters.setter - def filters(self, filters): - temp = Filter(self.domain) - 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: - handler(type(self).__name__, 'Filters {} is missing parameters: ' - '{}. Skipping.' - .format(filters, e)) - - @property - def sorting(self): - if self.__sorting != UNSETVALUE: - return self.__sorting - - @sorting.setter - def sorting(self, sorting): - typeChecker(type(self).__name__, sorting, int, "sorting") - categoryChecker(type(self).__name__, sorting, [0, 1, 2, 3], "sorting") - self.__sorting = sorting - - @property - def layout(self): - if self.__layout != UNSETVALUE: - return self.__layout - - @layout.setter - def layout(self, layout): - temp = Layout() - if "layout" in layout: - temp.layout = layout['layout'] - if "showName" in layout: - temp.showName = layout['showName'] - if "showID" in layout: - temp.showID = layout['showID'] - self.__layout = temp - - @property - def hideDisabled(self): - if self.__hideDisabled != UNSETVALUE: - return self.__hideDisabled - - @hideDisabled.setter - def hideDisabled(self, hideDisabled): - typeChecker(type(self).__name__, hideDisabled, bool, "hideDisabled") - self.__hideDisabled = hideDisabled - - @property - def techniques(self): - if self.__techniques != UNSETVALUE: - return self.__techniques - - @techniques.setter - def techniques(self, techniques): - typeChecker(type(self).__name__, techniques, list, "techniques") - self.__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 MissingParameters as e: - handler(type(self).__name__, 'Technique {} is missing parameters: ' - '{}. Skipping.' - .format(entry, e)) - - @property - def gradient(self): - if self.__gradient != UNSETVALUE: - return self.__gradient - - @gradient.setter - def gradient(self, gradient): - try: - 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): - if self.__legendItems != UNSETVALUE: - return self.__legendItems - - @legendItems.setter - def legendItems(self, legendItems): - typeChecker(type(self).__name__, legendItems, list, "legendItems") - self.__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 MissingParameters as e: - handler(type(self).__name__, 'Legend Item {} is missing parameters: ' - '{}. Skipping.' - .format(entry, e)) - - @property - def showTacticRowBackground(self): - if self.__showTacticRowBackground != UNSETVALUE: - return self.__showTacticRowBackground - - @showTacticRowBackground.setter - def showTacticRowBackground(self, showTacticRowBackground): - typeChecker(type(self).__name__, showTacticRowBackground, bool, - "showTacticRowBackground") - self.__showTacticRowBackground = showTacticRowBackground - - @property - def tacticRowBackground(self): - if self.__tacticRowBackground != UNSETVALUE: - return self.__tacticRowBackground - - @tacticRowBackground.setter - def tacticRowBackground(self, tacticRowBackground): - typeChecker(type(self).__name__, tacticRowBackground, str, - "tacticRowBackground") - self.__tacticRowBackground = tacticRowBackground - - @property - def selectTechniquesAcrossTactics(self): - if self.__selectTechniquesAcrossTactics != UNSETVALUE: - return self.__selectTechniquesAcrossTactics - - @selectTechniquesAcrossTactics.setter - def selectTechniquesAcrossTactics(self, selectTechniquesAcrossTactics): - typeChecker(type(self).__name__, selectTechniquesAcrossTactics, bool, - "selectTechniqueAcrossTactics") - self.__selectTechniquesAcrossTactics = selectTechniquesAcrossTactics - - @property - def selectSubtechniquesWithParent(self): - if self.__selectSubtechniquesWithParent != UNSETVALUE: - return self.__selectSubtechniquesWithParent - - @selectSubtechniquesWithParent.setter - def selectSubtechniquesWithParent(self, selectSubtechniquesWithParent): - typeChecker(type(self).__name__, selectSubtechniquesWithParent, bool, - "selectSubtechniquesWithParent") - self.__selectSubtechniquesWithParent = selectSubtechniquesWithParent - - @property - def metadata(self): - if self.__metadata != UNSETVALUE: - return self.__metadata - - @metadata.setter - def metadata(self, metadata): - typeChecker(type(self).__name__, metadata, list, "metadata") - self.__metadata = [] - for entry in metadata: - try: - if "divider" in entry: - self.__metadata.append(MetaDiv(entry["divider"])) - else: - loadChecker(type(self).__name__, entry, ['name', 'value'], "metadata") - self.__metadata.append(Metadata(entry['name'], entry['value'])) - except MissingParameters as e: - handler(type(self).__name__, 'Metadata {} is missing parameters: ' - '{}. Skipping.' - .format(entry, e)) - - def _enumerate(self): - """ - INTERNAL: Identifies which fields have been set for this Layer - object - :returns: a list of all set fields within this Layer object - """ - temp = ['name', 'version', 'domain'] - if self.description: - temp.append('description') - if self.filters: - temp.append('filters') - if self.sorting: - temp.append('sorting') - if self.layout: - temp.append('layout') - if self.hideDisabled: - temp.append('hideDisabled') - if self.techniques: - temp.append('techniques') - if self.gradient: - temp.append('gradient') - if self.legendItems: - temp.append('legendItems') - if self.showTacticRowBackground: - temp.append('showTacticRowBackground') - if self.tacticRowBackground: - temp.append('tacticRowBackground') - if self.selectTechniquesAcrossTactics: - temp.append('selectTechniquesAcrossTactics') - if self.selectSubtechniquesWithParent: - temp.append('selectSubtechniquesWithParent') - if self.metadata: - temp.append('metadata') - return temp - - 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, 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: - temp['sorting'] = self.sorting - if self.layout: - temp['layout'] = self.layout.get_dict() - if self.hideDisabled is not None: - temp['hideDisabled'] = self.hideDisabled - if self.techniques: - temp['techniques'] = [x.get_dict() for x in self.techniques] - if self.gradient: - temp['gradient'] = self.gradient.get_dict() - if self.legendItems: - temp['legendItems'] = [x.get_dict() for x in self.legendItems] - if self.showTacticRowBackground is not None: - temp['showTacticRowBackground'] = self.showTacticRowBackground - if self.tacticRowBackground: - temp['tacticRowBackground'] = self.tacticRowBackground - if self.selectTechniquesAcrossTactics is not None: - temp['selectTechniquesAcrossTactics'] = \ - self.selectTechniquesAcrossTactics - if self.selectSubtechniquesWithParent is not None: - temp['selectSubtechniquesWithParent'] = \ - self.selectSubtechniquesWithParent - if self.metadata: - temp['metadata'] = [x.get_dict() for x in self.metadata] - return temp - - def _linker(self, field, data): - """ - INTERNAL: Acts as a middleman routing the settings of values - within the layer - :param field: The value field being set - :param data: The corresponding data to set that field to - :raises UnknownLayerProperty: An error indicating that an - unexpected property was identified - """ - 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': - self.sorting = data - elif field == 'layout': - self.layout = data - elif field == 'hideDisabled': - self.hideDisabled = data - elif field == 'techniques': - self.techniques = data - elif field == 'gradient': - self.gradient = data - elif field == 'legendItems': - self.legendItems = data - elif field == 'showTacticRowBackground': - self.showTacticRowBackground = data - elif field == 'tacticRowBackground': - self.tacticRowBackground = data - elif field == 'selectTechniquesAcrossTactics': - self.selectTechniquesAcrossTactics = data - elif field == 'selectSubtechniquesWithParent': - self.selectSubtechniquesWithParent = data - elif field == 'metadata': - self.metadata = data - else: - handler(type(self).__name__, "Unknown layer property: {}" - .format(field)) - raise UnknownLayerProperty diff --git a/layers/core/layout.py b/layers/core/layout.py deleted file mode 100644 index 88619f53..00000000 --- a/layers/core/layout.py +++ /dev/null @@ -1,60 +0,0 @@ -try: - from ..core.exceptions import typeChecker, categoryChecker, UNSETVALUE -except ValueError: - from core.exceptions import typeChecker, categoryChecker, UNSETVALUE - - -class Layout: - def __init__(self): - """ - Initialization - Creates a layout object - """ - self.__layout = UNSETVALUE - self.__showID = UNSETVALUE - self.__showName = UNSETVALUE - - @property - def layout(self): - if self.__layout != UNSETVALUE: - return self.__layout - - @layout.setter - def layout(self, layout): - typeChecker(type(self).__name__, layout, str, "layout") - categoryChecker(type(self).__name__, layout, ["side", "flat", "mini"], - "layout") - self.__layout = layout - - @property - def showID(self): - if self.__showID != UNSETVALUE: - return self.__showID - - @showID.setter - def showID(self, showID): - typeChecker(type(self).__name__, showID, bool, "showID") - self.__showID = showID - - @property - def showName(self): - if self.__showName != UNSETVALUE: - return self.__showName - - @showName.setter - def showName(self, showName): - typeChecker(type(self).__name__, showName, bool, "showName") - self.__showName = showName - - def get_dict(self): - """ - Converts the currently loaded data into a dict - :returns: A dict representation of the local layout object - """ - listing = vars(self) - temp = dict() - for entry in listing: - if listing[entry] != UNSETVALUE: - temp[entry.split(type(self).__name__ + '__')[-1]]\ - = listing[entry] - if len(temp) > 0: - return temp diff --git a/layers/core/legenditem.py b/layers/core/legenditem.py deleted file mode 100644 index 5dd39689..00000000 --- a/layers/core/legenditem.py +++ /dev/null @@ -1,41 +0,0 @@ -try: - from ..core.exceptions import typeChecker -except ValueError: - from core.exceptions import typeChecker - - -class LegendItem: - def __init__(self, label, color): - """ - Initialization - Creates a legendItem object - - :param label: The label described by this object - :param color: The color associated with the label - """ - self.label = label - self.color = color - - @property - def color(self): - return self.__color - - @color.setter - def color(self, color): - typeChecker(type(self).__name__, color, str, "color") - self.__color = color - - @property - def label(self): - return self.__label - - @label.setter - def label(self, label): - typeChecker(type(self).__name__, label, str, "label") - self.__label = label - - def get_dict(self): - """ - Converts the currently loaded data into a dict - :returns: A dict representation of the local legendItem object - """ - return dict(label=self.__label, color=self.__color) diff --git a/layers/core/metadata.py b/layers/core/metadata.py deleted file mode 100644 index 281388d5..00000000 --- a/layers/core/metadata.py +++ /dev/null @@ -1,69 +0,0 @@ -try: - from ..core.exceptions import typeChecker -except ValueError: - from core.exceptions import typeChecker - - -class Metadata: - def __init__(self, name, value): - """ - Initialization - Creates a metadata object - - :param name: the name for this metadata entry - :param value: the corresponding value for this metadata entry - """ - self.name = name - self.value = value - - @property - def name(self): - return self.__name - - @name.setter - def name(self, name): - typeChecker(type(self).__name__, name, str, "name") - self.__name = name - - @property - def value(self): - return self.__value - - @value.setter - def value(self, value): - typeChecker(type(self).__name__, value, str, "value") - self.__value = value - - def get_dict(self): - """ - Converts the currently loaded data into a dict - :returns: A dict representation of the local metadata object - """ - return dict(name=self.__name, value=self.__value) - -class MetaDiv: - def __init__(self, active): - """ - Initialization - Creates a metadata object divider - """ - self.__name = "DIVIDER" - self.__value = active - - @property - def name(self): - return self.__name - - @property - def state(self): - return self.__value - - @state.setter - def state(self, state): - typeChecker(type(self).__name__, state, bool, "state") - self.__value = state - - def get_dict(self): - """ - Converts the currently loaded data into a dict - :returns: A dict representation of the local metadata object - """ - return dict(name=self.__name, value=self.__value) diff --git a/layers/core/technique.py b/layers/core/technique.py deleted file mode 100644 index ca954a3d..00000000 --- a/layers/core/technique.py +++ /dev/null @@ -1,171 +0,0 @@ -try: - from ..core.exceptions import BadInput, handler, typeChecker, \ - UNSETVALUE, UnknownTechniqueProperty, BadType - from ..core.metadata import Metadata, MetaDiv -except ValueError: - from core.exceptions import BadInput, handler, typeChecker, \ - UNSETVALUE, UnknownTechniqueProperty, BadType - from core.metadata import Metadata, MetaDiv - - -class Technique: - def __init__(self, tID): - """ - Initialization - Creates a technique object - - :param tID: The techniqueID associated with this technique object - """ - self.techniqueID = tID - self.__tactic = UNSETVALUE - self.__comment = UNSETVALUE - self.__enabled = UNSETVALUE - self.__score = UNSETVALUE - self.__color = UNSETVALUE - self.__metadata = UNSETVALUE - self.__showSubtechniques = UNSETVALUE - - @property - def techniqueID(self): - return self.__techniqueID - - @techniqueID.setter - def techniqueID(self, techniqueID): - typeChecker(type(self).__name__, techniqueID, str, "techniqueID") - if not techniqueID.startswith('T'): - handler(type(self).__name__, '{} not a valid value for techniqueID' - .format(techniqueID)) - raise BadInput - else: - self.__techniqueID = techniqueID - - @property - def tactic(self): - if self.__tactic != UNSETVALUE: - return self.__tactic - - @tactic.setter - def tactic(self, tactic): - typeChecker(type(self).__name__, tactic, str, "tactic") - self.__tactic = tactic - - @property - def comment(self): - if self.__comment != UNSETVALUE: - return self.__comment - - @comment.setter - def comment(self, comment): - typeChecker(type(self).__name__, comment, str, "comment") - self.__comment = comment - - @property - def enabled(self): - if self.__enabled != UNSETVALUE: - return self.__enabled - - @enabled.setter - def enabled(self, enabled): - typeChecker(type(self).__name__, enabled, bool, "enabled") - self.__enabled = enabled - - @property - def score(self): - if self.__score != UNSETVALUE: - return self.__score - - @score.setter - def score(self, score): - try: - typeChecker(type(self).__name__, score, int, "score") - self.__score = score - except BadType: - typeChecker(type(self).__name__, score, float, "score") - self.__score = int(score) - - @property - def color(self): - if self.__color != UNSETVALUE: - return self.__color - - @color.setter - def color(self, color): - typeChecker(type(self).__name__, color, str, "color") - self.__color = color - - @property - def metadata(self): - if self.metadata != UNSETVALUE: - return self.__metadata - - @metadata.setter - def metadata(self, metadata): - typeChecker(type(self).__name__, metadata, list, "metadata") - self.__metadata = [] - entry = "" - try: - for entry in metadata: - if "divider" in entry: - self.__metadata.append(MetaDiv(entry["divider"])) - else: - self.__metadata.append(Metadata(entry['name'], entry['value'])) - except KeyError as e: - handler(type(self).__name__, 'Metadata {} is missing parameters: ' - '{}. Unable to load.' - .format(entry, e)) - - @property - def showSubtechniques(self): - if self.__showSubtechniques != UNSETVALUE: - return self.__showSubtechniques - - @showSubtechniques.setter - def showSubtechniques(self, showSubtechniques): - typeChecker(type(self).__name__, showSubtechniques, bool, - "showSubtechniques") - self.__showSubtechniques = showSubtechniques - - def _loader(self, data): - """ - INTERNAL: Acts a middleman for loading values into the technique - object from a dict representation - :param data: A dict describing the technique - :raises UnknownTechniqueProperty: An error indicating that an - unexpected property was found on the technique - """ - for entry in data.keys(): - if entry == 'techniqueID': - pass - elif entry == 'tactic': - self.tactic = data[entry] - elif entry == 'comment': - self.comment = data[entry] - elif entry == 'enabled': - self.enabled = data[entry] - elif entry == 'score': - self.score = data[entry] - elif entry == 'color': - self.color = data[entry] - elif entry == 'metadata': - self.metadata = data[entry] - elif entry == 'showSubtechniques': - self.showSubtechniques = data[entry] - else: - handler(type(self).__name__, "Unknown technique property: {}" - .format(entry)) - raise UnknownTechniqueProperty - - def get_dict(self): - """ - Converts the currently loaded data into a dict - :returns: A dict representation of the local technique object - """ - dset = vars(self) - temp = {} - for key in dset: - entry = key.split(type(self).__name__ + '__')[-1] - if dset[key] != UNSETVALUE: - if entry != 'metadata': - temp[entry] = dset[key] - else: - temp[entry] = [x.get_dict() for x in dset[key]] - return temp diff --git a/layers/core/versions.py b/layers/core/versions.py deleted file mode 100644 index 7db525b5..00000000 --- a/layers/core/versions.py +++ /dev/null @@ -1,74 +0,0 @@ -try: - from ..core.exceptions import typeChecker, categoryChecker, UNSETVALUE, BadInput -except ValueError: - from core.exceptions import typeChecker, categoryChecker, UNSETVALUE, BadInput - - -class Versions: - def __init__(self, layer="4.1", attack=UNSETVALUE, navigator="4.1"): - """ - 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): - if self.__attack != UNSETVALUE: - return self.__attack - else: - return '4.x' - - @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") - try: - categoryChecker(type(self).__name__, navigator, ["4.0", "4.1"], "navigator version") - except BadInput: - print(f'[WARNING] - unrecognized navigator version {navigator}. Defaulting to the 4.1 schema, ' - f'this may result in unexpected behavior.') - self.__navigator = navigator - - @property - def layer(self): - return self.__layer - - @layer.setter - def layer(self, layer): - typeChecker(type(self).__name__, layer, str, "layer") - try: - categoryChecker(type(self).__name__, layer, ["3.0", "4.0", "4.1"], "layer version") - except BadInput: - print(f'[WARNING] - unrecognized layer version {layer}. Defaulting to the 4.1 schema, this may result in ' - f'unexpected behavior.') - if layer == '3.0': - print(f'[NOTICE] - Forcibly upgrading version from {layer} to 4.1.') - layer = "4.1" - 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/__init__.py b/layers/exporters/__init__.py deleted file mode 100644 index f5d7e105..00000000 --- a/layers/exporters/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .excel_templates import ExcelTemplates -from .matrix_gen import MatrixGen -from .to_excel import ToExcel -from .to_svg import ToSvg, SVGConfig -from .svg_templates import SvgTemplates \ No newline at end of file diff --git a/layers/exporters/excel_templates.py b/layers/exporters/excel_templates.py deleted file mode 100644 index c1ec3943..00000000 --- a/layers/exporters/excel_templates.py +++ /dev/null @@ -1,143 +0,0 @@ -import openpyxl -from openpyxl.styles import Font, Alignment, Border, Side, PatternFill -try: - from exporters.matrix_gen import MatrixGen -except ModuleNotFoundError: - from ..exporters.matrix_gen import MatrixGen - - -class BadTemplateException(Exception): - pass - -class ExcelTemplates: - - def __init__(self, source='taxii', local=None, domain='enterprise'): - """ - Initialization - Creates a ExcelTemplate object - - :param source: Source to use when compiling the matrix - :param local: Optional path to local stix data, required if source == "local" - :param domain: The domain to utilize - """ - muse = domain - if muse.startswith('mitre-'): - muse = domain[6:] - if muse.endswith('-attack'): - muse = domain[:-7] - if muse in ['enterprise', 'mobile']: - self.mode = muse - self.h = MatrixGen(source=source, local=local) - self.codex = self.h.get_matrix(muse) - else: - raise BadTemplateException - - def _build_raw(self, showName=True, showID=False, sort=0, scores=[], subtechs=[], exclude=[]): - """ - INTERNAL - builds a raw, not-yet-marked-up excel document based on the specifications - - :param showName: Whether or not to display names for each entry - :param showID: Whether or not to display Technique IDs for each entry - :param sort: The sort mode to use - :param subtechs: List of all visible subtechniques - :param exclude: List of of techniques to exclude from the matrix - :return: a openpyxl workbook object containing the raw matrix - """ - self.codex = self.h._adjust_ordering(self.codex, sort, scores) - template, joins = self.h._construct_panop(self.codex, subtechs, exclude) - self.template = template - wb = openpyxl.Workbook() - - sheet = wb.active - - header_template_f = Font(name='Calibri', bold=True) - header_template_a = Alignment(horizontal='center', vertical='bottom') - header_template_b = Border(bottom=Side(border_style='thin')) - header_template_c = PatternFill(patternType='solid', start_color='DDDDDD', end_color='DDDDDD') - - for entry in template: - c = sheet.cell(row=entry[0], column=entry[1]) - write_val = '' - if showName and showID: - write_val = self.h._get_ID(self.codex, template[entry]) + ': ' + template[entry] - elif showName: - write_val = template[entry] - elif showID: - write_val = self.h._get_ID(self.codex, template[entry]) - c.value = write_val - if entry[0] == 1: - c.font = header_template_f - c.alignment = header_template_a - c.border = header_template_b - c.fill = header_template_c - - ## patch widths - dims = {} - sheet_handle = wb.active - for row in sheet_handle: - for cell in row: - if cell.value: - dims[cell.column_letter] = max((dims.get(cell.column_letter, 0), len(str(cell.value)))) - for col, value in dims.items(): - sheet_handle.column_dimensions[col].width = value - - merge_border_thickness = 'thin' - merge_template_l = Border(bottom=Side(border_style=merge_border_thickness), - left=Side(border_style=merge_border_thickness), - top=Side(border_style=merge_border_thickness)) - merge_template_r = Border(bottom=Side(border_style=merge_border_thickness), - right=Side(border_style=merge_border_thickness), - top=Side(border_style=merge_border_thickness)) - - for marker in joins: - sheet_handle.merge_cells(start_row=marker[0], start_column=marker[1], end_row=marker[0] + marker[2] - 1, - end_column=marker[1]) - for block in range(marker[0], marker[0] + marker[2]): - sheet_handle[block][marker[1]].border = merge_template_r - sheet_handle[block][marker[1] - 1].border = merge_template_l - sheet_handle.merge_cells(start_row=1, start_column=marker[1], end_row=1, end_column=marker[1] + 1) - adjust = sheet_handle.cell(row=marker[0], column=marker[1]) - adjust.alignment = Alignment(vertical='top') - - return wb - - def export(self, showName, showID, sort=0, scores=[], subtechs=[], exclude=[]): - """ - Export a raw customized excel template - - - :param showName: Whether or not to display names for each entry - :param showID: Whether or not to display Technique IDs for each entry - :param sort: The sort mode to utilize - :param subtechs: List of all visible subtechniques - :param exclude: List of of techniques to exclude from the matrix - return: a openpyxl workbook object containing the raw matrix - """ - return self._build_raw(showName, showID, sort, scores, subtechs, exclude) - - def retrieve_coords(self, techniqueID, tactic=None): - """ - Locate the openpyxl coordinates of the provided technique for the currently loaded matrix - - :param techniqueID: The ID of the technique to locate - :param tactic: Optional parameter to isolate the technique to a specific tactic - :return: A tuple representing the (row, column) of the target element in the workbook - """ - listing = [] - match = self.h._get_name(self.codex, techniqueID) - for entry in self.template: - if self.template[entry] == match: - if tactic is not None: - try: - if self.template[(1, entry[1])] != self.h.convert(tactic): - continue - except KeyError: - # account for subtechniques when scanning - if self.template[(1, entry[1] - 1)] != self.h.convert(tactic): - continue - listing.append(entry) - if listing == []: - if '.' in techniqueID: - parent = self.retrieve_coords(techniqueID.split('.')[0], tactic) - if parent != []: - return 'HIDDEN' - return listing diff --git a/layers/exporters/fonts/monospace.ttf b/layers/exporters/fonts/monospace.ttf deleted file mode 100644 index 556d2fd5..00000000 Binary files a/layers/exporters/fonts/monospace.ttf and /dev/null differ diff --git a/layers/exporters/fonts/sans-serif.ttf b/layers/exporters/fonts/sans-serif.ttf deleted file mode 100644 index 718f22d4..00000000 Binary files a/layers/exporters/fonts/sans-serif.ttf and /dev/null differ diff --git a/layers/exporters/fonts/serif.ttf b/layers/exporters/fonts/serif.ttf deleted file mode 100644 index 4c86f19a..00000000 Binary files a/layers/exporters/fonts/serif.ttf and /dev/null differ diff --git a/layers/exporters/matrix_gen.py b/layers/exporters/matrix_gen.py deleted file mode 100644 index 36b763de..00000000 --- a/layers/exporters/matrix_gen.py +++ /dev/null @@ -1,353 +0,0 @@ -from stix2 import TAXIICollectionSource, Filter, MemoryStore -from taxii2client.v20 import Server, Collection - - -class DomainNotLoadedError(Exception): - pass - -class MatrixEntry: - def __init__(self, id=None, name=None): - if id is not None: - self.id = id - if name is not None: - self.name = name - self.score = None - - @property - def id(self): - if self.__id is not None: - return self.__id - - @id.setter - def id(self, id): - self.__id = id - - @property - def name(self): - if self.__name is not None: - return self.__name - - @name.setter - def name(self, name): - self.__name = name - - @property - def score(self): - if self.__score is not None: - return self.__score - - @score.setter - def score(self, score): - self.__score = score - -class Tactic: - def __init__(self, tactic=None, techniques=None, subtechniques=None): - if tactic is not None: - self.tactic = tactic - if techniques is not None: - self.techniques = techniques - if subtechniques is not None: - self.subtechniques = subtechniques - - @property - def tactic(self): - if self.__tactic is not None: - return self.__tactic - - @tactic.setter - def tactic(self, tactic): - self.__tactic = tactic - - @property - def techniques(self): - if self.__techniques is not None: - return self.__techniques - - @techniques.setter - def techniques(self, techniques): - self.__techniques = techniques - - @property - def subtechniques(self): - if self.__subtechniques is not None: - return self.__subtechniques - - @subtechniques.setter - def subtechniques(self, subtechniques): - self.__subtechniques = subtechniques - -class MatrixGen: - def __init__(self, source='taxii', local=None): - """ - Initialization - Creates a matrix generator object - - :param server: Source to utilize (taxii or local) - :param local: string path to local cache of stix data - """ - self.convert_data = {} - if source.lower() not in ['taxii', 'local']: - print('[MatrixGen] - Unable to generate matrix, source {} is not one of "taxii" or "local"'.format(source)) - raise ValueError - - if source.lower() == 'taxii': - self.server = Server('https://cti-taxii.mitre.org/taxii') - self.api_root = self.server.api_roots[0] - self.collections = dict() - for collection in self.api_root.collections: - if collection.title != "PRE-ATT&CK": - tc = Collection('https://cti-taxii.mitre.org/stix/collections/' + collection.id) - self.collections[collection.title.split(' ')[0].lower()] = TAXIICollectionSource(tc) - elif source.lower() == 'local': - if local is not None: - hd = MemoryStore() - if 'mobile' in local.lower(): - self.collections['mobile'] = hd.load_from_file(local) - else: - self.collections['enterprise'] = hd.load_from_file(local) - else: - print('[MatrixGen] - "local" source specified, but path to local source not provided') - raise ValueError - self.matrix = {} - self._build_matrix() - - @staticmethod - def _remove_revoked_deprecated(content): - """Remove any revoked or deprecated objects from queries made to the data source""" - return list( - filter( - lambda x: x.get("x_mitre_deprecated", False) is False and x.get("revoked", False) is False, - content - ) - ) - - def _search(self, domain, query): - interum = self.collections[domain].query(query) - return self._remove_revoked_deprecated(interum) - - def _get_tactic_listing(self, domain='enterprise'): - """ - INTERNAL - retrieves tactics for the associated domain - - :param domain: The domain to draw from - """ - tactics = {} - t_filt = [] - matrix = self._search(domain, [Filter('type', '=', 'x-mitre-matrix')]) - for i in range(len(matrix)): - tactics[matrix[i]['name']] = [] - for tactic_id in matrix[i]['tactic_refs']: - tactics[matrix[i]['name']].append(self._search(domain,([Filter('id', '=', tactic_id)]))[0]) - for entry in tactics[matrix[0]['name']]: - self.convert_data[entry['x_mitre_shortname']] = entry['name'] - self.convert_data[entry['name']] = entry['x_mitre_shortname'] - t_filt.append(MatrixEntry(id=entry['external_references'][0]['external_id'], name=entry['name'])) - return t_filt - - def _get_technique_listing(self, tactic, domain='enterprise'): - """ - INTERNAL - retrieves techniques for a given tactic and domain - - :param tactic: The tactic to grab techniques from - :param domain: The domain to draw from - """ - techniques = [] - subtechs = {} - techs = self._search(domain,[Filter('type', '=', 'attack-pattern'), - Filter('kill_chain_phases.phase_name', '=', tactic)]) - for entry in techs: - if entry['kill_chain_phases'][0]['kill_chain_name'] == 'mitre-attack' or \ - entry['kill_chain_phases'][0]['kill_chain_name'] == 'mitre-mobile-attack': - tid = [t['external_id'] for t in entry['external_references'] if 'attack' in t['source_name']] - if '.' not in tid[0]: - techniques.append(MatrixEntry(id=tid[0], name=entry['name'])) - else: - parent = tid[0].split('.')[0] - if parent not in subtechs: - subtechs[parent] = [] - subtechs[parent].append(MatrixEntry(id=tid[0], name=entry['name'])) - return techniques, subtechs - - def _adjust_ordering(self, codex, mode, scores=[]): - """ - INTERNAL - Adjust ordering of matrix based on sort mode - - :param codex: The pre-existing matrix data - :param mode: The sort mode to use - :param scores: Any relevant scores to use in modes 2, 3 - """ - if mode == 0: - return codex - if mode == 1: - for colm in codex: - colm.technique.reverse() - for sub in colm.subtechniques: - colm.subtechniques[sub].reverse() - return codex - for colm in codex: - for st in colm.subtechniques: - for sub in colm.subtechniques[st]: - sub.score = 0 - for entry in scores: - if entry[0] == sub.id and (entry[1] == False or entry[1] == self.convert(colm.tactic.name)): - sub.score = entry[2] - break - for tech in colm.techniques: - tech.score = 0 - for entry in scores: - if entry[0] == tech.id and (entry[1] == False or entry[1] == self.convert(colm.tactic.name)): - tech.score = entry[2] - break - if mode == 2: - for colm in codex: - for tsub in colm.subtechniques: - colm.subtechniques[tsub].sort(key=lambda x: x.score) - colm.techniques.sort(key=lambda x: x.score) - if mode == 3: - for colm in codex: - for tsub in colm.subtechniques: - colm.subtechniques[tsub].sort(key=lambda x: x.score, reverse=True) - colm.techniques.sort(key=lambda x: x.score, reverse=True) - return codex - - def _construct_panop(self, codex, subtechs, excludes): - """ - INTERNAL - Creates a list of lists template for the matrix layout - - :param codex: A list of lists matrix (output of .get_matrix()) - :param subtechs: A list of subtechniques that will be visible - :param excludes: A list of techniques that will be excluded - """ - st = [x[0] for x in subtechs] - s_tacs = [x[1] for x in subtechs] - et = [x[0] for x in excludes] - e_tacs = [x[1] for x in excludes] - - matrix_obj = {} - column = 0 - cycle = False - to_add = [] - stechs = [] - joins = [] - for col in codex: - # each column of the matrix - column += 1 - if cycle: - for entry in to_add: - sr = entry[0] - joins.append([entry[0], column-1, len(stechs[entry[1]])]) - for element in stechs[entry[1]]: - matrix_obj[(sr, column)] = element.name - sr += 1 - cycle = False - column += 1 - row = 2 - matrix_obj[(1, column)] = col.tactic.name - c_name = col.tactic.name - stechs = col.subtechniques - to_add = [] - for element in col.techniques: - elname = element.name - tid = element.id - skip = False - for entry in range(0, len(et)): - if et[entry] == tid and (e_tacs[entry] == False or self.convert(e_tacs[entry]) == c_name): - skip = True - break - if not skip: - matrix_obj[(row, column)] = elname - sat = False - for entry in range(0, len(st)): - if st[entry] == tid and (s_tacs[entry] == False or self.convert(s_tacs[entry]) == c_name): - # this tech has enabled subtechs - to_add.append((row, tid)) - row += len(stechs[tid]) - cycle = True - sat = True - break - if not sat: - row += 1 - return matrix_obj, joins - - def _get_ID(self, codex, name): - """ - INTERNAL - Do lookups to retrieve the ID of a technique given it's name - - :param codex: The list of lists matrix object (output of get_matrix) - :param name: The name of the technique to retrieve the ID of - :return: The ID of the technique referenced by name - """ - for col in codex: - if col.tactic.name == name: - return col.tactic.id - for entry in col.subtechniques: - for subtech in col.subtechniques[entry]: - if subtech.name == name: - return subtech.id - for entry in col.techniques: - if entry.name == name: - return entry.id - return '' - - def _get_name(self, codex, id): - """ - INTERNAL - Do lookups to retrieve the name of a technique given it's ID - - :param codex: The list of lists matrix object (output of get_matrix) - :param id: The ID of the technique to retrieve the name of - :return: The name of the technique referenced by id - """ - for col in codex: - if col.tactic.id == id: - return col.tactic.name - for entry in col.subtechniques: - for subtech in col.subtechniques[entry]: - if subtech.id == id: - return subtech.name - for entry in col.techniques: - if entry.id == id: - return entry.name - return '' - - def convert(self, input): - """ - Convert tactic names to and from short names - - :param input: A tactic normal or short name - :return: The tactic's short or normal name - """ - if self.convert_data == {}: - return None - if input in self.convert_data: - return self.convert_data[input] - - def _build_matrix(self, domain='enterprise'): - """ - Build a ATT&CK matrix object, as a list of lists containing technique dictionaries - - :param domain: The domain to build a matrix for - """ - if domain not in self.collections: - raise DomainNotLoadedError - self.matrix[domain] = [] - tacs = self._get_tactic_listing(domain) - for tac in tacs: - techs, subtechs = self._get_technique_listing(tac.name.lower().replace(' ', '-'), domain) - stemp = {} - # sort subtechniques via id, append to column - for par in subtechs: - subtechs[par].sort(key=lambda x: x.name) - stemp[par] = subtechs[par] - # sort techniques alphabetically, append to column - techs.sort(key=lambda x: x.name) - colm = Tactic(tactic=tac, techniques=techs, subtechniques=stemp) - self.matrix[domain].append(colm) - - def get_matrix(self, domain='enterprise'): - """ - Retrieve an ATT&CK Domain object - - :param domain: The domain to build a matrix for - """ - if domain not in self.matrix: - self._build_matrix(domain) - return self.matrix[domain] diff --git a/layers/exporters/svg_objects.py b/layers/exporters/svg_objects.py deleted file mode 100644 index 96d752c5..00000000 --- a/layers/exporters/svg_objects.py +++ /dev/null @@ -1,441 +0,0 @@ -import drawSvg as draw -import colorsys -import numpy as np -import os -from PIL import ImageFont - -try: - from core.gradient import Gradient -except ModuleNotFoundError: - from ..core.gradient import Gradient - - -def convertToPx(quantity, unit): - """ - INTERNAL: Convert values to pixels - - :param quantity: value - :param unit: unit for that value - :return: quantity in pixels - """ - if unit == "in": - return quantity * 96 - if unit == "cm": - return quantity * 37.79375 - if unit == "px": - return quantity - if unit == "em": - return quantity * 16 - if unit == "pt": - return quantity * 1.33 - return -1 - -def _getstringwidth(string, font, size): - """ - INTERNAL: Calculate the width of a string (in pixels) - - :param string: string to evaluate - :param font: font to use - :param size: font size - :return: pixel length of string - """ - font = ImageFont.truetype('{}/fonts/{}.ttf'.format(os.path.sep.join(__file__.split(os.path.sep)[:-1]), font), - int(size)) - length, _ = font.getsize(string) - return length - -def _getstringheight(string, font, size): - """ - INTERNAL: Calculate the width of a string (in pixels) - - :param string: string to evaluate - :param font: font to use - :param size: font size - :return: pixel height of string - """ - font = ImageFont.truetype('{}/fonts/{}.ttf'.format(os.path.sep.join(__file__.split(os.path.sep)[:-1]), font), - int(size)) - _, height = font.getsize(string) - return height - -def _findSpace(words, width, height, maxFontSize): - """ - INTERNAL: Find space locations for a string to keep it within width x height - - :param words: string to evaluate - :param width: width of the box - :param height: height of the box - :param maxFontSize: maximum font size - :return: - """ - padding = 4 - breakDistance = min(height, (maxFontSize + 3) * len(words)) - - breakTextHeight = breakDistance / len(words) - fitTextHeight = min(breakTextHeight, height) * 0.8 - - longestWordLength = -9999 - fitTextWidth = 9999 - for w in range(0, len(words)): - word = words[w] - longestWordLength = max(longestWordLength, len(word)) - try: - fitTextWidth = ((width - (2 * padding)) / longestWordLength * 1.45) - except: - pass - size = min(maxFontSize, fitTextHeight, fitTextWidth) - return size - - -def _find_breaks(num_spaces, num_breaks=3): - """ - INTERNAL: Generate break mapping - - :param num_spaces: number of spaces in string - :param num_breaks: number of breaks to insert - :return: list of possible break mappings - """ - breaks = set() - - def recurse(breakset_inherit, depth, num_breaks): - """recursive combinatorics; breakset is binary array of break locations; depth is the depth of recursion, - num_breaks is how many breaks should be added""" - for i in range(len(breakset_inherit)): # for each possible break - # insert a break here - breakset = np.copy(breakset_inherit) - breakset[i] = 1 - breaks.add("".join(str(x) for x in breakset)) - # try inserting another depth of break - if depth < num_breaks - 1: recurse(breakset, depth + 1, num_breaks) - - initial_breaks = [0] * num_spaces - breaks.add("".join(str(x) for x in initial_breaks)) - recurse(initial_breaks, 0, num_breaks) - - return breaks - - -def _optimalFontSize(st, width, height, maxFontSize=12): - """ - INTERNAL: Calculate the optimal fontsize and word layout for a box of width x height - - :param st: string to fit - :param width: box width - :param height: box height - :param maxFontSize: maximum allowable font size - :return: size in pixels for font, array of strings split by where new lines should go - """ - words = st.split(" ") - bestSize = -9999 - bestWordArrangement = [] - - num_spaces = len(words) - num_breaks = 1 - if num_spaces < 20: - num_breaks = 2 - elif num_spaces < 50: - num_breaks = 3 - - if _findSpace([st], width, height, maxFontSize) == maxFontSize: - return maxFontSize, [st] - breaks = _find_breaks(num_spaces, num_breaks) - for binaryString in breaks: - wordSet = [] - - for k in range(0, len(binaryString)): - if binaryString[k] == "0": - if len(wordSet) == 0: - wordSet.append(words[k]) - else: - wordSet[len(wordSet) - 1] = wordSet[len(wordSet) - 1] + " " + words[k] - else: - wordSet.append(words[k]) - - size = _findSpace(wordSet, width, height, maxFontSize) - if size > bestSize: - bestSize = size - bestWordArrangement = wordSet - #if size == maxFontSize: - # break - - return bestSize, bestWordArrangement - -class Cell(draw.DrawingParentElement): - TAG_NAME = 'rect' - def __init__(self, height, width, fill, tBC, ctype=None): - # tBC = tableBorderColor, ctype='class' field on resulting svg object, fill=[R,G,B] - super().__init__(height=height, width=width, style='fill: rgb({}, {}, {})'.format(fill[0], fill[1], fill[2]), - stroke=tBC) - if ctype: - self.args['class'] = ctype - -class HeaderRect(draw.DrawingParentElement): - TAG_NAME = 'rect' - def __init__(self, width, height, ctype, x=None, y=None, outline=True): - # ctype='class' field on resulting svg object, x=x coord, y=y coord - super().__init__(width=width, height=height, fill='white', rx='5') - self.args['class'] = ctype - if x: - self.args['x'] = x - if y: - self.args['y'] = y - if outline: - self.args['stroke'] = 'black' - -class G(draw.DrawingParentElement): - TAG_NAME = 'g' - def __init__(self, tx=None, ty=None, style=None, ctype=None): - # tx=translate x, ty=translate y, ctype='class' field on resulting svg object - super().__init__() - if tx is None: - tx = 0 - if ty is None: - ty = 0 - self.args['transform']='translate(' + str(tx) +',' + str(ty) +')' - if style: - self.args['style'] = style - if ctype: - self.args['class'] = ctype - -class Line(draw.DrawingParentElement): - TAG_NAME = 'line' - def __init__(self, x1, x2, y1, y2, stroke): - # x1=start x, x2=stop x, y1=start y, y2=stop y, stroke='stroke' field on resulting svg object - super().__init__(x1=x1, x2=x2, y1=y1, y2=y2, stroke=stroke) - -class Text(draw.Text): - def __init__(self, text, font_size, ctype, position=None, tx=None, ty=None, x=None, y=None, fill=None): - # ctype='class' object on resulting svg, position='text-anchor' field, tx/ty=translate x/y, x/y=x/y coord - if x is None: - x = 0 - if y is None: - y = 0 - super().__init__(text=text, fontSize=font_size, x=x, y=-y) - self.args['class'] = ctype - if tx is None: - tx = 0 - if ty is None: - ty = 0 - if tx != 0 or ty != 0: - self.args['transform']='translate(' + str(tx) +',' + str(ty) +')' - if position: - self.args['style'] = 'text-anchor: {}'.format(position) - if fill: - self.args['fill'] = fill - -class Swatch(draw.DrawingParentElement): - TAG_NAME = 'rect' - def __init__(self, height, width, fill): - # fill= [R,G,B] - super().__init__(height=height, width=width, style='fill: rgb({}, {}, {})'.format(fill[0], fill[1], fill[2])) - - -class SVG_HeaderBlock: - @staticmethod - def build(height, width, label, config, variant='text', t1text=None, t2text=None, colors=[]): - """ - Build a single SVG Header Block object - - :param height: Height of the block - :param width: Width of the block - :param label: Label for the block - :param config: SVG configuration object - :param variant: text or graphic - the type of header block to build - :param t1text: upper text - :param t2text: lower text - :param colors: array of tuple (color, score value) for the graphic variant - :return: - """ - g = G(ty=5) - rect = HeaderRect(width, height, 'header-box') - g.append(rect) - rect2 = HeaderRect(_getstringwidth(label, config.font, 12), _getstringheight(label, config.font, 12), - 'label-cover', x=7, y=-5, outline=False) - g.append(rect2) - text = Text(label, 12, 'header-box-label', x=8, y=3) - g.append(text) - internal = G(tx=5, ctype='header-box-content') - g.append(internal) - if variant == 'text': - upper = G(tx=0, ty=2.1) - internal.append(upper) - if t1text is not None: - bu = t2text != None and t2text != "" - theight = (height-5) - if bu: - theight = theight / 2 - fs, patch_text = _optimalFontSize(t1text, width, theight, maxFontSize=28) - lines = len(patch_text) - y = theight/2 + 2.1 - if lines > 1: - y = y - (theight / 5 * (lines - 1) - (fs * 6/16)) - if float(fs) < (convertToPx(config.border, config.unit) + 2.1): - y = theight/2 + 2.1 - (theight / 5) - t1 = Text("\n".join(patch_text), fs, '', x=4, y=y) - upper.append(t1) - if bu: - upper.append(Line(0, width - 10, theight, theight, stroke='#dddddd')) - upper_fs = fs - lower_offset = (theight + 2.1) - lower = G(tx=0, ty= lower_offset) - fs, patch_text = _optimalFontSize(t2text, width, (height - (height/3 + upper_fs)), maxFontSize=28) - y = theight / 2 + 2.1 - lines = len(patch_text) - adju = "\n".join(patch_text) - if lines > 1: - y = y - ((theight / 5) * (lines - 1)) - if float(fs) > lower_offset: - y = y + 2*(float(fs) - lower_offset) - t2 = Text(adju, fs, '', x=4, y=y) - lower.append(t2) - internal.append(lower) - else: - if len(colors): - usable = width - 10 - block_width = usable/len(colors) - sub1 = G(ty=5) - sub2 = G(ty=5) - internal.append(sub1) - sub1.append(sub2) - cells = G(ctype="legendCells") - sub2.append(cells) - offset = 0 - for entry in colors: - cell = G(ctype="cell", tx=offset) - conv = entry[0] - if conv.startswith('#'): - conv = conv[1:] - block = Swatch(15, block_width, tuple(int(conv[i:i+2], 16) for i in (0, 2, 4))) - offset += block_width - cell.append(block) - cells.append(cell) - tblob = str(entry[1]) - off = (block_width-(5 * (1+ len(tblob))))/2 - if off < 0: - off = 0 - fs, _ = _optimalFontSize("0", width/len(colors), height) - label = Text(tblob, fs, ctype='label', ty=25, tx=off) - cell.append(label) - return g - -class SVG_Technique: - def __init__(self, gradient): - self.grade = gradient - if self.grade == None: - self.grade = Gradient(colors=["#ff6666", "#ffe766", "#8ec843"], minValue=1, maxValue=100) - - def build(self, offset, technique, height, width, tBC, subtechniques=[], mode=(True, False), tactic=None, - colors=[]): - """ - Build a SVG Technique block - - :param offset: Current offset to build the block at (so it fits in the column) - :param technique: The technique to build a block for - :param height: The height of the technique block - :param width: The width of the technique block - :param tBC: The hex code of the technique block's border - :param subtechniques: List of any visible subtechniques for this technique - :param mode: Display mode (Show Name, Show ID) - :param tactic: The corresponding tactic - :param colors: List of all default color values if no score can be found - :return: The newly created SVG technique block - """ - g = G(ty=offset) - c = self._com_color(technique, tactic, colors) - t = dict(name=self._disp(technique.name, technique.id, mode), id=technique.id, - color=tuple(int(c[i:i+2], 16) for i in (0, 2, 4))) - tech, text = self._block(t, height, width, tBC=tBC) - g.append(tech) - g.append(text) - new_offset = height - for entry in subtechniques: - gp = G(tx=width/5, ty=new_offset) - g.append(gp) - c = self._com_color(entry, tactic, colors) - st = dict(name=self._disp(entry.name, entry.id, mode), id=entry.id, - color=tuple(int(c[i:i + 2], 16) for i in (0, 2, 4))) - subtech, subtext = self._block(st, height, width - width/5, tBC=tBC) - gp.append(subtech) - gp.append(subtext) - new_offset = new_offset + height - if len(subtechniques): - g.append(draw.Lines(width/16, -height, - width/8, -height * 2, - width/8, -height * (len(subtechniques) + 1), - width/5, -height * (len(subtechniques) + 1), - width/5, -height, - close=True, - fill=tBC, - stroke=tBC)) - return g, offset + new_offset - - @staticmethod - def _block(technique, height, width, tBC): - """ - INTERNAL: Build a technique block element - - :param technique: Technique data dictionary - :param height: Block height - :param width: Block width - :param tBC: Block border color - :return: Block object, fit text object - """ - tech = Cell(height, width, technique['color'], ctype=technique['id'], tBC=tBC) - - fs, patch_text = _optimalFontSize(technique['name'], width, height) - adjusted = "\n".join(patch_text) - - lines = adjusted.count('\n') - - y = height / 2 - if lines > 0: - y = (height - (lines * fs)) / 2 + height/10 #padding - else: - y = y + fs / 4 - - hls = colorsys.rgb_to_hls(technique['color'][0], technique['color'][1], technique['color'][2]) - fill = None - if hls[1] < 127.5: - fill = 'white' - - text = Text(adjusted.encode('utf-8').decode('ascii', 'backslashreplace'), fs, '', x=4, y=y, fill=fill) - return tech, text - - def _com_color(self, technique, tactic, colors=[]): - """ - INTERNAL: Retrieve hex color for a block - - :param technique: Technique object - :param tactic: What tactic the technique falls under - :param colors: Default technique color data - :return: Hex color code - """ - c = 'FFFFFF' - if technique.score: - c = self.grade.compute_color(technique.score)[1:] - else: - for x in colors: - if x[0] == technique.id and (x[1] == tactic or not x[1]): - c = x[2][1:] - return c - - @staticmethod - def _disp(name, id, mode): - """ - INTERNAL: Generate technique display form - - :param name: The name of the technique - :param id: The ID of the technique - :param mode: Which mode to use - :return: Target display string for the technique - """ - p1 = name - p2 = id - if not mode[0]: - p1 = '' - if not mode[1]: - p2 = '' - out = ': '.join([p2, p1]) - if out.startswith(': '): - return p1 - return out \ No newline at end of file diff --git a/layers/exporters/svg_templates.py b/layers/exporters/svg_templates.py deleted file mode 100644 index 7f22be0f..00000000 --- a/layers/exporters/svg_templates.py +++ /dev/null @@ -1,304 +0,0 @@ -import warnings -with warnings.catch_warnings(): - warnings.simplefilter("ignore") - import drawSvg as draw - -try: - 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 -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 - - -class BadTemplateException(Exception): - pass - - -class SvgTemplates: - def __init__(self, source='taxii', domain='enterprise', local=None): - """ - Initialization - Creates a SvgTemplate object - - :param domain: Which domain to utilize for the underlying matrix layout - :param source: Use the taxii server or local data - :param local: Optional path to local stix data - """ - muse = domain - if muse.startswith('mitre-'): - muse = domain[6:] - if muse.endswith('-attack'): - muse = domain[:-7] - if muse in ['enterprise', 'mobile']: - self.mode = muse - self.h = MatrixGen(source=source, local=local) - self.codex = self.h.get_matrix(muse) - self.lhandle = None - else: - raise BadTemplateException - - def _build_headers(self, name, config, domain='Enterprise', version='8', desc=None, filters=None, gradient=None): - """ - Internal - build the header blocks for the svg - - :param name: The name of the layer being exported - :param config: SVG Config object - :param domain: The layer's domain - :param version: The layer's version - :param desc: Description of the layer being exported - :param filters: Any filters applied to the layer being exported - :param gradient: Gradient information included with the layer - :return: Instantiated SVG header - """ - max_x = convertToPx(config.width, config.unit) - max_y = convertToPx(config.height, config.unit) - header_height = convertToPx(config.headerHeight, config.unit) - ff = config.font - d = draw.Drawing(max_x, max_y, origin=(0, -max_y), displayInline=False) - psych = 0 - overlay = None - if config.showHeader: - border = convertToPx(config.border, config.unit) - root = G(tx=border, ty=border, style='font-family: {}'.format(ff)) - - header = G() - root.append(header) - b1 = G() - header.append(b1) - - header_count = 0 - if config.showAbout: - header_count += 1 - if config.showFilters: - header_count += 1 - if config.showDomain: - header_count += 1 - if config.showLegend and gradient is not False and config.legendDocked: - header_count += 1 - - operation_x = (max_x - border) - (1.5 * border * (header_count - 1)) - border - if header_count > 0: - header_width = operation_x / header_count - if config.showAbout: - if desc is not None: - g = SVG_HeaderBlock().build(height=header_height, width=header_width, label='about', - t1text=name, t2text=desc, config=config) - else: - g = SVG_HeaderBlock().build(height=header_height, width=header_width, label='about', - t1text=name, config=config) - b1.append(g) - psych += 1 - if config.showDomain: - if domain.startswith('mitre-'): - domain = domain[6:].capitalize() - if domain.endswith('-attack'): - domain = domain[:-7].capitalize() - tag = domain + ' ATT&CK v' + version - gD = SVG_HeaderBlock().build(height=header_height, width=header_width, label='domain', - t1text=tag, config=config) - bD = G(tx=operation_x / header_count * psych + 1.5 * border * psych) - header.append(bD) - bD.append(gD) - psych += 1 - if config.showFilters: - fi = filters - if fi is None: - 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) - b2 = G(tx=operation_x / header_count * psych + 1.5 * border * psych) - header.append(b2) - b2.append(g2) - psych += 1 - if config.showLegend and gradient is not False: - gr = gradient - if gr is None: - gr = Gradient(colors=["#ff6666","#ffe766","#8ec843"], minValue=1, maxValue=100) - colors = [] - div = round((gr.maxValue - gr.minValue) / (len(gr.colors) * 2 - 1)) - for i in range(0, len(gr.colors) * 2 - 1): - colors.append( - (gr.compute_color(int(gr.minValue + div * i)), gr.minValue + div * i)) - colors.append((gr.compute_color(gr.maxValue), gr.maxValue)) - if config.legendDocked: - b3 = G(tx=operation_x / header_count * psych + 1.5 * border * psych) - g3 = SVG_HeaderBlock().build(height=header_height, width=header_width, label='legend', - variant='graphic', colors=colors, config=config) - header.append(b3) - b3.append(g3) - psych +=1 - else: - adjusted_height = convertToPx(config.legendHeight, config.unit) - adjusted_width = convertToPx(config.legendWidth, config.unit) - g3 = SVG_HeaderBlock().build(height=adjusted_height, width=adjusted_width, label='legend', - variant='graphic', colors=colors, config=config) - lx = convertToPx(config.legendX, config.unit) - if not lx: - lx = max_x - adjusted_width - convertToPx(config.border, config.unit) - ly = convertToPx(config.legendY, config.unit) - if not ly: - ly = max_y - adjusted_height - convertToPx(config.border, config.unit) - overlay = G(tx=lx, ty=ly) - if (ly + adjusted_height) > max_y or (lx + adjusted_width) > max_x: - print("[WARNING] - Floating legend will render partly out of view...") - overlay.append(g3) - d.append(root) - return d, psych, overlay - - def get_tactic(self, tactic, height, width, config, colors=[], scores=[], subtechs=[], exclude=[], - mode=(True, False)): - """ - Build a 'tactic column' svg object - - :param tactic: The corresponding tactic for this column - :param height: A technique block's allocated height - :param width: A technique blocks' allocated width - :param config: A SVG Config object - :param colors: Default color data in case of no score - :param scores: Score values for the dataset - :param subtechs: List of visible subtechniques - :param exclude: List of excluded techniques - :param mode: Tuple describing text for techniques (Show Name, Show ID) - :return: Instantiated tactic column (or none if no techniques were found) - """ - offset = 0 - column = G(ty=2) - for a in tactic.subtechniques: - self._copy_scores(tactic.subtechniques[a],scores,tactic.tactic.name, exclude) - for x in tactic.techniques: - if any(x.id == y[0] and (y[1] == self.h.convert(tactic.tactic.name) or not y[1]) for y in exclude): - continue - self._copy_scores([x], scores, tactic.tactic.name, exclude) - if any(x.id == y[0] and (y[1] == self.h.convert(tactic.tactic.name) or not y[1]) for y in subtechs): - a, offset = self.get_tech(offset, mode, x, tactic=self.h.convert(tactic.tactic.name), - subtechniques=tactic.subtechniques.get(x.id, []), colors=colors, - config=config, height=height, width=width) - else: - a, offset = self.get_tech(offset, mode, x, tactic=self.h.convert(tactic.tactic.name), - subtechniques=[], colors=colors, config=config, height=height, width=width) - column.append(a) - if len(column.children) == 0: - return None - return column - - def get_tech(self, offset, mode, technique, tactic, config, height, width, subtechniques=[], colors=[]): - """ - Retrieve a svg object for a single technique - - :param offset: The offset in the column based on previous work - :param mode: Tuple describing display format (Show Name, Show ID) - :param technique: The technique to build a block for - :param tactic: The corresponding tactic - :param config: An SVG Config object - :param height: The allocated height of a technique block - :param width: The allocated width of a technique block - :param subtechniques: A list of all visible subtechniques, some of which may apply to this one - :param colors: A list of all color overrides in the event of no score, which may apply - :return: Tuple (SVG block, new offset) - """ - a, b = SVG_Technique(self.lhandle.gradient).build(offset, technique, height, - width, subtechniques=subtechniques, mode=mode, - tactic=tactic, colors=colors, tBC=config.tableBorderColor) - return a, b - - def export(self, showName, showID, lhandle, config, sort=0, scores=[], colors=[], subtechs=[], exclude=[]): - """ - Export a layer object to an SVG object - - :param showName: Boolean of whether or not to show names - :param showID: Boolean of whether or not to show IDs - :param lhandle: The layer object being exported - :param config: A SVG Config object - :param sort: The sort mode - :param scores: List of tactic scores - :param colors: List of tactic default colors - :param subtechs: List of visible subtechniques - :param exclude: List of excluded techniques - :return: - """ - grad = False - if len(scores): - grad = lhandle.gradient - d, presence, overlay = self._build_headers(lhandle.name, config, lhandle.domain, - lhandle.versions.attack, lhandle.description, lhandle.filters, - grad) - self.codex = self.h._adjust_ordering(self.codex, sort, scores) - self.lhandle = lhandle - index = 0 - lengths = [] - border = convertToPx(config.border, config.unit) - glob = G(tx=border) - for x in self.codex: - su = len(x.techniques) - for enum in exclude: - if enum[0] in [y.id for y in x.techniques]: - if self.h.convert(enum[1]) == x.tactic.name or enum[1] == False: - su -= 1 - for y in x.subtechniques: - if y in [z[0] for z in subtechs]: - su += len(x.subtechniques[y]) - lengths.append(su) - tech_width = ((convertToPx(config.width, config.unit) - 2.2 * border) / sum([1 for x in lengths if x > 0])) - \ - border - header_offset = convertToPx(config.headerHeight, config.unit) - if presence == 0: - header_offset = 0 - header_offset += 2.5 * border - tech_height = (convertToPx(config.height, config.unit) - header_offset - - convertToPx(config.border, config.unit)) / (max(lengths) + 1) - incre = tech_width + 1.1 * border - for x in self.codex: - disp = '' - if showName and showID: - disp = x.tactic.id + ": " + x.tactic.name - elif showName: - disp = x.tactic.name - elif showID: - disp = x.tactic.id - - g = G(tx=index, ty=header_offset) - - fs, _ = _optimalFontSize(disp, tech_width, tech_height, maxFontSize=28) - tx = Text(ctype='TacticName', font_size=fs, text=disp, position='middle') - gt = G(tx=(tech_width)/2, ty=(tech_height)/2) - gt.append(tx) - a = self.get_tactic(x, tech_height, tech_width, colors=colors, subtechs=subtechs, exclude=exclude, - mode=(showName, showID), scores=scores, config=config) - b = G(ty=tech_height) - g.append(gt) - b.append(a) - g.append(b) - if a: - glob.append(g) - index += incre - d.append(glob) - if overlay: - d.append(overlay) - return d - - - def _copy_scores(self, listing, scores, tactic_name, exclude): - """ - INTERNAL: Move scores over from the input object (scores) to the one used to build the zvg (listing) - - :param listing: List of objects to apply scores to - :param scores: List of scores for this tactic - :param exclude: List of excluded techniques - :return: None - operates on the raw object itself - """ - for b in listing: - found = False - for y in scores: - if b.id == y[0] and (y[1] == self.h.convert(tactic_name) or not y[1]): - b.score = y[2] - found = True - continue - if not found: - b.score = None \ No newline at end of file diff --git a/layers/exporters/to_excel.py b/layers/exporters/to_excel.py deleted file mode 100644 index 321e0769..00000000 --- a/layers/exporters/to_excel.py +++ /dev/null @@ -1,124 +0,0 @@ -from openpyxl.comments import Comment -from openpyxl.styles import PatternFill, Font -import colorsys - -try: - from core import Layer - from exporters import ExcelTemplates -except ModuleNotFoundError: - from ..core import Layer - from ..exporters import ExcelTemplates - -class ToExcel: - def __init__(self, domain='enterprise', source='taxii', local=None): - """ - Sets up exporting system, builds underlying matrix - - :param source: Source to generate the matrix from, one of (taxii or local) - :param local: Optional path to local stix data, required in source is local - - """ - self.domain = domain - self.raw_handle = ExcelTemplates(domain=domain, source=source, local=local) - - def to_xlsx(self, layer, filepath="layer.xlsx"): - """ - Exports a layer file to the excel format as the file specified - - :param layer: A layer to initialize the instance with - :param filepath: The location to write the excel file to - """ - - if not isinstance(layer, Layer): - raise TypeError - - if self.domain not in layer.layer.domain: - raise ValueError(f"layer domain ({layer.layer.domain}) does not match exporter domain ({self.domain})") - - included_subs = [] - if layer.layer.techniques: - for entry in layer.layer.techniques: - if entry.showSubtechniques: - if entry.tactic: - included_subs.append((entry.techniqueID, entry.tactic)) - else: - included_subs.append((entry.techniqueID, False)) - - excluded = [] - if layer.layer.hideDisabled: - for entry in layer.layer.techniques: - if entry.enabled == False: - if entry.tactic: - excluded.append((entry.techniqueID, entry.tactic)) - else: - excluded.append((entry.techniqueID, False)) - scores = [] - if layer.layer.techniques: - for entry in layer.layer.techniques: - if entry.score: - if entry.tactic: - scores.append((entry.techniqueID, entry.tactic, entry.score)) - else: - scores.append((entry.techniqueID, False, entry.score)) - sName = True - sID = False - sort = 0 - if layer.layer.layout: - sName = layer.layer.layout.showName - sID = layer.layer.layout.showID - if layer.layer.sorting: - sort = layer.layer.sorting - raw_template = self.raw_handle.export(showName=sName, showID=sID, sort=sort, scores=scores, - subtechs=included_subs, exclude=excluded) - sheet_obj = raw_template.active - sheet_obj.title = layer.layer.name - for tech in layer.layer.techniques: - p_tactic = None - if tech.tactic: - p_tactic = tech.tactic - coords = self.raw_handle.retrieve_coords(tech.techniqueID, p_tactic) - if coords == [] or coords == 'HIDDEN': - tac = p_tactic - if tac == None: - tac = "(none)" - if coords == []: - print('WARNING! Technique/Tactic ' + tech.techniqueID + '/' + tac + - ' does not appear to exist in the loaded matrix. Skipping...') - else: - parents = [x for x in layer.layer.techniques if x.techniqueID == tech.techniqueID.split('.')[0]] - if tech.tactic: - parents = [x for x in parents if x.tactic == tech.tactic] - if all([True if not x.showSubtechniques else False for x in parents]): - print('NOTE! Technique/Tactic ' + tech.techniqueID + '/' + tac + ' does not appear ' - 'to be visible in the matrix. Its parent appears to be hiding it.') - else: - print('WARNING! Technique/Tactic ' + tech.techniqueID + '/' + tac + ' seems malformed. ' - 'Skipping...') - continue - for location in coords: - cell = sheet_obj.cell(row=location[0],column=location[1]) - if tech.comment: - cell.comment = Comment(tech.comment, 'ATT&CK Scripts Exporter') - - if tech.enabled == False: - if layer.layer.hideDisabled: - pass - else: - grayed_out = Font(color='909090') - cell.font = grayed_out - continue - if tech.color: - c_color = PatternFill(fill_type='solid', start_color=tech.color.upper()[1:]) - cell.fill = c_color - continue - if tech.score: - if layer.layer.gradient: - comp_color = layer.layer.gradient.compute_color(tech.score) - c_color = PatternFill(fill_type='solid', start_color=comp_color.upper()[1:]) - cell.fill = c_color - RGB = tuple(int(comp_color.upper()[1:][i:i+2], 16) for i in (0, 2, 4)) - hls = colorsys.rgb_to_hls(RGB[0], RGB[1], RGB[2]) - if hls[1] < 127.5: - white = Font(color='FFFFFF') - cell.font = white - raw_template.save(filepath) diff --git a/layers/exporters/to_svg.py b/layers/exporters/to_svg.py deleted file mode 100644 index ab6b2701..00000000 --- a/layers/exporters/to_svg.py +++ /dev/null @@ -1,466 +0,0 @@ -import json - -try: - from core import Layer - from exporters.svg_templates import SvgTemplates -except ModuleNotFoundError: - from ..core import Layer - from ..exporters.svg_templates import SvgTemplates - -from layers.core import Layer as topLayer # alternative import for typechecking - -class NoLayer(Exception): - pass - - -class SVGConfig: - d_width=8.5 - d_height=11 - d_headerHeight=1 - d_unit="in" - d_showSubtechniques="expanded" - d_font="sans-serif" - d_tableBorderColor="#6B7279" - d_showHeader=True - d_legendDocked=True - d_legendX=0 - d_legendY=0 - d_legendWidth=2 - d_legendHeight=1 - d_showLegend=True - d_showFilters=True - d_showAbout=True - d_showDomain=True - d_border=0.104 - - def __init__(self, width=d_width, height=d_height, headerHeight=d_headerHeight, unit=d_unit, - showSubtechniques=d_showSubtechniques, font=d_font, tableBorderColor=d_tableBorderColor, - showHeader=d_showHeader, legendDocked=d_legendDocked, legendX=d_legendX, legendY=d_legendY, - legendWidth=d_legendWidth, legendHeight=d_legendHeight, showLegend=d_showLegend, - showFilters=d_showFilters, showAbout=d_showAbout, showDomain=d_showDomain, border=d_border): - """ - Define parameters to configure SVG export - - :param width: Desired SVG width - :param height: Desired SVG height - :param headerHeight: Desired Header Block height - :param unit: SVG measurement units (qualifies width, height, etc.) - "in", "cm", "px", "em", or "pt" - :param showSubtechniques: Display form for subtechniques - "all", "expanded" (decided by layer), or "none" - :param font: What font style to use - "serif", "sans-serif", or "monospace" - :param tableBorderColor: Hex color to use for the technique borders - :param showHeader: Whether or not to show Header Blocks - :param legendDocked: Whether or not the legend should be docked - :param legendX: Where to place the legend on the x axis if not docked - :param legendY: Where to place the legend on the y axis if not docked - :param legendWidth: Width of the legend if not docked - :param legendHeight: Height of the legend if not docked - :param showLegend: Whether or not to show the legend - :param showFilters: Whether or not to show the Filter Header Block - :param showAbout: Whether or not to show the About Header Block - :param showDomain: Whether or not to show the Domain Version Header Block - :param border: What default border width to use - """ - # force defaults in case bad values are provided so we don't crash later - self.width = self.d_width - self.height = self.d_height - self.headerHeight = self.d_headerHeight - self.unit = self.d_unit - self.showSubtechniques = self.d_showSubtechniques - self.font = self.d_font - self.tableBorderColor = self.d_tableBorderColor - self.showHeader = self.d_showHeader - self.legendDocked = self.d_legendDocked - self.legendX = self.d_legendX - self.legendY = self.d_legendY - self.legendWidth = self.d_legendWidth - self.legendHeight = self.d_legendHeight - self.showDomain = self.d_showDomain - self.showLegend = self.d_showLegend - self.showFilters = self.d_showFilters - self.showAbout = self.d_showAbout - self.border = self.d_border - - self.width = width - self.height = height - self.headerHeight = headerHeight - self.unit = unit - self.showSubtechniques = showSubtechniques - self.font = font - self.tableBorderColor = tableBorderColor - self.showHeader = showHeader - self.legendDocked = legendDocked - self.legendX = legendX - self.legendY = legendY - self.legendWidth = legendWidth - self.legendHeight = legendHeight - self.showDomain = showDomain - self.showLegend = showLegend - self.showFilters = showFilters - self.showAbout = showAbout - self.border = border - - def load_from_file(self, filename=""): - """ - Load config from a json file - - :param filename: The file to read from - """ - with open(filename, 'r') as fio: - raw = fio.read() - self._data = json.loads(raw) - for entry in self._data: - patched = entry - if not patched.startswith('_SVGConfig__'): - patched = '_SVGConfig__' + patched - if patched in vars(self).keys(): - setattr(self, entry, self._data[entry]) - else: - print('WARNING - Unidentified Config Field in {}: {}'.format(filename, entry)) - - self.__str__() - - def save_to_file(self, filename=""): - """ - Store config to json file - - :param filename: The file to write to - """ - out = dict(width=self.width, height=self.height, headerHeight=self.headerHeight, unit=self.unit, - showSubtechniques=self.showSubtechniques, font=self.font, tableBorderColor=self.tableBorderColor, - showHeader=self.showHeader, legendDocked=self.legendDocked, legendX=self.legendX, - legendY=self.legendY, legendWidth=self.legendWidth, legendHeight=self.legendHeight, - showLegend=self.showLegend, showFilters=self.showFilters, showAbout=self.showAbout, - border=self.border) - with open(filename, 'w') as file: - json.dump(out, file) - - def __str__(self): - """ - INTERNAL - display current configuration - """ - print("SVGConfig current settings: ") - print("width - {}".format(self.width)) - print("height - {}".format(self.height)) - print("headerHeight - {}".format(self.headerHeight)) - print("unit - {}".format(self.unit)) - print("showSubtechniques - {}".format(self.showSubtechniques)) - print("font - {}".format(self.font)) - print("tableBorderColor - {}".format(self.tableBorderColor)) - print("showHeader - {}".format(self.showHeader)) - print("legendDocked - {}".format(self.legendDocked)) - print("legendX - {}".format(self.legendX)) - print("legendY - {}".format(self.legendY)) - print("legendWidth - {}".format(self.legendWidth)) - print("legendHeight - {}".format(self.legendHeight)) - print("showLegend - {}".format(self.showLegend)) - print("showFilters - {}".format(self.showFilters)) - print("showAbout - {}".format(self.showAbout)) - print("border - {}".format(self.border)) - - @property - def width(self): - if self.__width is not None: - return self.__width - - @width.setter - def width(self, width): - if isinstance(width, int) or isinstance(width, float): - self.__width = width - else: - print('[Warning] - Unable to set width to {}: not a float or int'.format(width)) - - @property - def height(self): - if self.__height is not None: - return self.__height - - @height.setter - def height(self, height): - if isinstance(height, int) or isinstance(height, float): - self.__height = height - else: - print('[Warning] - Unable to set height to {}: not a float or int'.format(height)) - - @property - def headerHeight(self): - if self.__headerHeight is not None: - return self.__headerHeight - - @headerHeight.setter - def headerHeight(self, headerHeight): - if isinstance(headerHeight, int) or isinstance(headerHeight, float): - self.__headerHeight = headerHeight - else: - print('[Warning] - Unable to set headerHeight to {}: not a float or int'.format(headerHeight)) - - @property - def unit(self): - if self.__unit is not None: - return self.__unit - - @unit.setter - def unit(self, unit): - if unit in ['in', 'cm', 'px', 'em', 'pt']: - self.__unit = unit - else: - print('[Warning] - Unable to set unit to {}: not one of ["in", "cm", "px", "em", "pt"]'.format(unit)) - - @property - def showSubtechniques(self): - if self.__showSubtechniques is not None: - return self.__showSubtechniques - - @showSubtechniques.setter - def showSubtechniques(self, showSubtechniques): - if showSubtechniques in ["expanded", "all", "none"]: - self.__showSubtechniques = showSubtechniques - else: - print('[Warning] - Unable to set showSubtechniques to {}: not one of ["expanded", "all", "none"]'.format( - showSubtechniques)) - - @property - def font(self): - if self.__font is not None: - return self.__font - - @font.setter - def font(self, font): - if font in ["serif", "sans-serif", "monospace"]: - self.__font = font - else: - print('[Warning] - Unable to set font to {}: not one of ["serif", "sans-serif", "monospace"]'.format(font)) - - @property - def tableBorderColor(self): - if self.__tableBorderColor is not None: - return self.__tableBorderColor - - @tableBorderColor.setter - def tableBorderColor(self, tableBorderColor): - if isinstance(tableBorderColor, str) and tableBorderColor.startswith('#') and len(tableBorderColor) == 7: - self.__tableBorderColor = tableBorderColor - else: - reason = '' - if not isinstance(tableBorderColor, str): - reason = 'not a string' - elif not tableBorderColor.startswith('#'): - reason = 'not a valid code (does not start with #)' - elif len(tableBorderColor) != 7: - reason = 'not a valid code (#ZZZZZZ)' - print('[Warning] - Unable to set tableBorderColor to {}: '.format(tableBorderColor) + reason) - - @property - def showHeader(self): - if self.__showHeader is not None: - return self.__showHeader - - @showHeader.setter - def showHeader(self, showHeader): - if isinstance(showHeader, bool): - self.__showHeader = showHeader - else: - print('[Warning] - Unable to set showHeader to {}: not a bool'.format(showHeader)) - - @property - def legendDocked(self): - if self.__legendDocked is not None: - return self.__legendDocked - - @legendDocked.setter - def legendDocked(self, legendDocked): - if isinstance(legendDocked, bool): - self.__legendDocked = legendDocked - else: - print('[Warning] - Unable to set legendDocked to {}: not a bool'.format(legendDocked)) - - @property - def legendX(self): - if self.__legendX is not None: - return self.__legendX - - @legendX.setter - def legendX(self, legendX): - if isinstance(legendX, int) or isinstance(legendX, float): - self.__legendX = legendX - else: - print('[Warning] - Unable to set legendX to {}: not a float or int'.format(legendX)) - - @property - def legendY(self): - if self.__legendY is not None: - return self.__legendY - - @legendY.setter - def legendY(self, legendY): - if isinstance(legendY, int) or isinstance(legendY, float): - self.__legendY = legendY - else: - print('[Warning] - Unable to set legendY to {}: not a float or int'.format(legendY)) - - @property - def legendWidth(self): - if self.__legendWidth is not None: - return self.__legendWidth - - @legendWidth.setter - def legendWidth(self, legendWidth): - if isinstance(legendWidth, int) or isinstance(legendWidth, float): - self.__legendWidth = legendWidth - else: - print('[Warning] - Unable to set legendWidth to {}: not a float or int'.format(legendWidth)) - - @property - def legendHeight(self): - if self.__legendHeight is not None: - return self.__legendHeight - - @legendHeight.setter - def legendHeight(self, legendHeight): - if isinstance(legendHeight, int) or isinstance(legendHeight, float): - self.__legendHeight = legendHeight - else: - print('[Warning] - Unable to set legendHeight to {}: not a float or int'.format(legendHeight)) - - @property - def showLegend(self): - if self.__showLegend is not None: - return self.__showLegend - - @showLegend.setter - def showLegend(self, showLegend): - if isinstance(showLegend, bool): - self.__showLegend = showLegend - else: - print('[Warning] - Unable to set showLegend to {}: not a bool'.format(showLegend - )) - - @property - def showFilters(self): - if self.__showFilters is not None: - return self.__showFilters - - @showFilters.setter - def showFilters(self, showFilters): - if isinstance(showFilters, bool): - self.__showFilters = showFilters - else: - print('[Warning] - Unable to set showFilters to {}: not a bool'.format(showFilters)) - - @property - def showAbout(self): - if self.__showAbout is not None: - return self.__showAbout - - @showAbout.setter - def showAbout(self, showAbout): - if isinstance(showAbout, bool): - self.__showAbout = showAbout - else: - print('[Warning] - Unable to set showAbout to {}: not a bool'.format(showAbout)) - - @property - def showDomain(self): - if self.__showDomain is not None: - return self.__showDomain - - @showDomain.setter - def showDomain(self, showDomain): - if isinstance(showDomain, bool): - self.__showDomain = showDomain - else: - print('[Warning] - Unable to set showAbout to {}: not a bool'.format(showDomain)) - - @property - def border(self): - if self.__border is not None: - return self.__border - - @border.setter - def border(self, border): - if isinstance(border, float): - self.__border = border - else: - print('[Warning] - Unable to set border to {}: not a float'.format(border)) - -class ToSvg: - def __init__(self, domain='enterprise', source='taxii', local=None, config=None): - """ - Sets up exporting system, builds underlying matrix - - :param domain: Which domain to utilize for the underlying matrix layout - :param source: Use the taxii server or local data - :param local: Optional path to local stix data - :param config: Optional pre-existing SVGConfig object - """ - self.raw_handle = SvgTemplates(domain=domain, source=source, local=local) - if config != None and isinstance(config, SVGConfig): - self.config = config - else: - self.config = SVGConfig() - - def to_svg(self, layer, filepath='example.svg'): - """ - Generate a svg file based on the input layer - - :param layer: Input attack layer object to transform - :param filepath: Desired output svg location - :return: (meta) svg file at the targeted output location - """ - if layer is not None: - if not isinstance(layer, Layer) and not isinstance(layer, topLayer): - raise TypeError - - if layer is None: - raise NoLayer - - included_subs = [] - if layer.layer.techniques: - for entry in layer.layer.techniques: - if self.config.showSubtechniques == "expanded": - if entry.showSubtechniques: - if entry.tactic: - included_subs.append((entry.techniqueID, entry.tactic)) - else: - included_subs.append((entry.techniqueID, False)) - elif self.config.showSubtechniques == "all": - if entry.tactic: - included_subs.append((entry.techniqueID, entry.tactic)) - else: - included_subs.append((entry.techniqueID, False)) - else: # none displayed - pass - - excluded = [] - if layer.layer.hideDisabled: - for entry in layer.layer.techniques: - if entry.enabled == False: - if entry.tactic: - excluded.append((entry.techniqueID, entry.tactic)) - else: - excluded.append((entry.techniqueID, False)) - scores = [] - colors = [] - if layer.layer.techniques: - for entry in layer.layer.techniques: - if entry.score: - if entry.tactic: - scores.append((entry.techniqueID, entry.tactic, entry.score)) - else: - scores.append((entry.techniqueID, False, entry.score)) - elif entry.color: - if entry.tactic: - colors.append((entry.techniqueID, entry.tactic, entry.color)) - else: - colors.append((entry.techniqueID, False, entry.color)) - sName = True - sID = False - sort = 0 - if layer.layer.layout: - sName = layer.layer.layout.showName - sID = layer.layer.layout.showID - if layer.layer.sorting: - sort = layer.layer.sorting - d = self.raw_handle.export(showName=sName, showID=sID, sort=sort, scores=scores, - subtechs=included_subs, colors=colors, - exclude=excluded, lhandle=layer.layer, config=self.config) - d.saveSvg(filepath) diff --git a/layers/layerExporter_cli.py b/layers/layerExporter_cli.py deleted file mode 100644 index 8c59fb52..00000000 --- a/layers/layerExporter_cli.py +++ /dev/null @@ -1,54 +0,0 @@ -import argparse -from exporters.to_svg import ToSvg, SVGConfig -from exporters.to_excel import ToExcel -from core import Layer - -if __name__ == '__main__': - parser = argparse.ArgumentParser(description='Export an ATT&CK Navigator layer as a svg image or excel file') - parser.add_argument('-m', '--mode', choices=['svg', 'excel'], required=True, help='The form to export the layers in') - parser.add_argument('input', nargs='+', help='Path(s) to the file to export') - parser.add_argument('-s','--source', choices=['taxii', 'local'], default='taxii', help='What source to utilize when building the matrix') - parser.add_argument('--local', help='Path to the local resource if --source=local', default=None) - parser.add_argument('-o','--output', nargs='+', help='Path(s) to the exported svg/xlsx file', required=True) - parser.add_argument('-l', '--load_settings', help='[SVG Only] Path to a SVG configuration json to use when ' - 'rendering', default=None) - parser.add_argument('-d', '--size', nargs=2, help='[SVG Only] X and Y size values (in inches) for SVG export (use ' - '-l for other settings)', default=[8.5, 11], metavar=("WIDTH", - "HEIGHT")) - args = parser.parse_args() - if len(args.output) != len(args.input): - print('Mismatched number of output file paths to input file paths. Exiting...') - - for i in range(0, len(args.input)): - entry = args.input[i] - print('{}/{} - Beginning processing {}'.format(i + 1, len(args.input), entry)) - lay = Layer() - try: - lay.from_file(entry) - except: - print('Unable to load {}. Skipping...'.format(entry)) - continue - if args.mode=='excel': - if not args.output[i].endswith('.xlsx'): - print('[ERROR] Unable to export {} as type: excel to {}'.format(entry, args.output[i])) - continue - exy = ToExcel(domain=lay.layer.domain, source=args.source, local=args.local) - exy.to_xlsx(lay, filepath=args.output[i]) - else: - if not args.output[i].endswith('.svg'): - print('[ERROR] Unable to export {} as type: svg to {}'.format(entry, args.output[i])) - continue - conf = SVGConfig() - if args.load_settings: - conf.load_from_file(args.load_settings) - if len(args.size) == 2: - conf.width = float(args.size[0]) - conf.height = float(args.size[1]) - svy = ToSvg(domain=lay.layer.domain, source=args.source, local=args.local, config=conf) - svy.to_svg(lay, filepath=args.output[i]) - print('{}/{} - Finished exporting {} to {}'.format(i + 1, len(args.input), entry, args.output[i])) - - -else: - print('This script is intended as a commandline wrapper for exporting scripts in the library. ' - 'It will only work if run as the main script.') \ No newline at end of file diff --git a/layers/manipulators/__init__.py b/layers/manipulators/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/layers/manipulators/layerops.py b/layers/manipulators/layerops.py deleted file mode 100644 index fb644f12..00000000 --- a/layers/manipulators/layerops.py +++ /dev/null @@ -1,362 +0,0 @@ -# Example Use: -# from layers.manipulators.layerops import LayerOps -# from layers.core.layer import Layer - -# demo = Layer() -# demo.load_file("C:\Users\attack\Downloads\layer.json") -# demo2 = Layer() -# demo2.load_input(Existing_Layer_String_Previously_Loaded) - -# lo = LayerOps(score=lambda x: x[0] * x[1], name=lambda x: x[1], -# desc=lambda x: "This is an list example") -# out_layer = lo.process([demo, demo2]) -# out_layer.export_file("C:\demo_layer1.json") - -# lo2 = LayerOps(score=lambda x: x['a'], color=lambda x: x['b'], -# desc=lambda x: "This is a dict example") -# out_layer2 = lo2.process({'a': demo, 'b': demo2}) -# dict_layer = out_layer2.get_dict() -# print(dict_layer) -# out_layer2.export_file("C:\demo_layer2.json") - -import copy -try: - from core import Layer -except ModuleNotFoundError: - from ..core import Layer - - -class InvalidFormat(Exception): - pass - - -class BadLambda(Exception): - pass - - -class MismatchedDomain(Exception): - pass - - -class LayerOps: - def __init__(self, score=None, comment=None, enabled=None, colors=None, - metadata=None, name=None, desc=None, default_values=None): - """ - Initialization - configures the object to handle processing - based on user provided Lambdas - :param score: lambda to calculate score - :param comment: lambda to generate comments - :param enabled: lambda to determine enabled status - :param metadata: lambda to generate metadata - :param name: new name to apply to the resulting layer - :param desc: new description to apply to the resulting layer - :param default_values: dictionary containing desired default - values for missing data element values - """ - self._score = score - self._comment = comment - self._enabled = enabled - self._colors = colors - self._metadata = metadata - self._name = name - self._desc = desc - self._default_values = { - "comment": "", - "enabled": True, - "color": "#ffffff", - "score": 1, - "metadata": [] - } - if default_values is not None: - for entry in default_values: - self._default_values[entry] = default_values[entry] - - def process(self, data, default_values=None): - """ - takes a list or dict of Layer objects, and processes them - :param data: A dict or list of Layer objects. - :param default_values: dictionary containing desired default values for - missing data element values - :raises InvalidFormat: An error indicating that the layer data - wasn't provided in a list or dict - """ - if isinstance(data, dict): - temp = {} - for entry in data.keys(): - temp[entry] = data[entry].layer.techniques - self.mode = 'dict' - out = self._merge_to_template(data, key=list(data.keys())[0]) - elif isinstance(data, list): - temp = [] - for entry in data: - temp.append(entry.layer.techniques) - self.mode = 'list' - out = self._merge_to_template(data) - else: - raise InvalidFormat - da = temp - corpus = self._build_template(temp) - - defaults = self._default_values - if default_values is not None: - for entry in default_values: - defaults[entry] = default_values[entry] - - return self._compute(data, da, corpus, out, defaults) - - def _compute(self, data, da, corpus, out, defaults): - """ - INTERNAL: Applies the configured lambda to the dataset - :param data: the dataset being processed - :param da: extracted techniques from the dataset, sorted by - dataset format - :param corpus: master list of combined techniques and - technique data - :param out: baseline template for the new layer - :param defaults: default values each technique should use if a - field is missing - :returns: a Layer object representing the resultant layer - """ - composite = copy.deepcopy(corpus) - if self._score is not None: - for entry in composite: - entry['score'] = self._applyOperation(da, entry, 'score', - self._score, defaults) - - if self._comment is not None: - for entry in composite: - entry['comment'] = self._applyOperation(da, entry, 'comment', - self._comment, defaults) - - if self._enabled is not None: - for entry in composite: - entry['enabled'] = self._applyOperation(da, entry, 'enabled', - self._enabled, defaults) - - if self._colors is not None: - for entry in composite: - entry['color'] = self._applyOperation(da, entry, 'color', - self._colors, defaults) - - if self._metadata is not None: - for entry in composite: - entry['metadata'] = self._applyOperation(da, entry, 'metadata', - self._metadata, - defaults) - - processed = copy.deepcopy(out) - processed['techniques'] = composite - if self._name is not None: - processed['name'] = self._applyOperation(data, None, 'name', - self._name, defaults, - glob='name') - - if self._desc is not None: - processed['description'] = self._applyOperation(data, None, - 'description', - self._desc, - defaults, - glob='description') - - return Layer(processed) - - def _merge_to_template(self, data, key=0): - """ - INTERNAL: merges initial layer files in either dict or list form - into a single template. Defaults to the first entry in the - case of difference in metadata. - :param key: the key referencing the first entry to default to - :raises MismatchedDomain: An error indicating that the layers - came from different domains - """ - out = {} - dict_map = [] - collide = [] - if self.mode == 'dict': - for x in data.keys(): - dict_map.append(x) - collide.append(data[x]) - else: - for x in data: - collide.append(x) - key_space = data[key].layer._enumerate() - _raw = data[key].layer.get_dict() - for entry in key_space: - if entry != 'techniques': - standard = _raw[entry] - if any(y != standard for y in - [getattr(x.layer, entry) for x in collide]): - if entry == 'domain': - print('FATAL ERROR! Layer mis-match on domain. ' - 'Exiting.') - raise MismatchedDomain - print('Warning! Layer mis-match detected for {}. ' - 'Defaulting to {}\'s value'.format(entry, key)) - out[entry] = standard - return out - - def _build_template(self, data): - """ - INTERNAL: builds a base template by combining available technique - listings from each layer - :param data: the raw ingested technique data (list or dict) - """ - if self.mode == 'list': - return self._template(data) - elif self.mode == 'dict': - temp = {} - t2 = [] - for key in data: - temp[key] = self._template([data[key]]) - for key in temp: - for elm in temp[key]: - if not any(elm['techniqueID'] == x['techniqueID'] - and elm['tactic'] == x['tactic'] - for x in t2): - t2.append(elm) - else: - [x.update(elm) - if x['techniqueID'] == elm['techniqueID'] - and elm['tactic'] == x['tactic'] - else x for x in t2] - return t2 - - def _template(self, data): - """ - INTERNAL: creates a template technique entry for a given listing - of techniques - :param data: a single layer's technique data - :returns: a list of technique templates - """ - temp = [] - for entry in data: - temp.append([{"techniqueID": x.techniqueID, "tactic": x.tactic} - if x.tactic else {"techniqueID": x.techniqueID} - for x in entry]) - complete = [] - for entry in temp: - for val in entry: - if val in complete: - continue - complete.append(val) - return complete - - def _grabList(self, search, collection): - """ - INTERNAL: generates a list containing all values for a given key - across the collection - :param search: the key to search for - :param collection: the data collection to search - :returns: a list of values for that key across the collection - """ - temp = [] - for x in collection: - temp.append(self._grabElement(search, x)) - return temp - - def _grabDict(self, search, collection): - """ - INTERNAL: generates a dictionary containing all values for a given - key across the collection - :param search: the key to search for - :param collection: the data collection to search - :returns: a dict of values for that key across the collection - """ - temp = {} - for key in collection: - temp[key] = self._grabElement(search, collection[key]) - return temp - - def _grabElement(self, search, listing): - """ - INTERNAL: returns a matching element in the listing for the - search key - :param search: the key to search for - :param listing: the data element to search - :returns: the found value, or an empty dict - """ - val = [x for x in listing if self._inDict(search, x)] - if len(val) > 0: - return val[0] - return {} - - def _inDict(self, search, complete): - """ - INTERNAL: returns bool of whether or not the key searched - for can be found across the dataset corpus - :param search: the key to search for - :param complete: the data set to search for - :returns: true/false - """ - comp_list = complete.get_dict().items() - search_terms = search.items() - return all(elm in comp_list for elm in search_terms) - - def _applyOperation(self, corpus, element, name, lda, defaults, glob=None): - """ - INTERNAL: applies a lambda expression to the dataset - :param corpus: the dataset - :param element: the template file to fill out - :param name: the name of the field being processed - :param lda: the lambda expression to apply - :param defaults: any default values should the field not be found - in a dataset entry - :raises BadLambda: error denoting that an error has occurred when - running the lambda function - :returns: lambda output - """ - if self.mode == 'list': - if glob: - listing = [getattr(x.layer, glob) for x in corpus] - listing = [{name: x} for x in listing] - else: - te = dict(techniqueID=element['techniqueID']) - if 'tactic' in element: - te['tactic'] = element['tactic'] - listing = self._grabList(te, corpus) - listing = [x.get_dict() if not isinstance(x, dict) - else dict() for x in listing] - values = [] - for x in listing: - if name in x: - values.append(x[name]) - else: - if defaults is not None: - if name in defaults: - values.append(defaults[name]) - continue - values.append(self._default_values[name]) - else: - values = {} - if glob: - listing = {} - for k in corpus.keys(): - listing[k] = {glob: getattr(corpus[k].layer, glob)} - else: - te = dict(techniqueID=element['techniqueID']) - if 'tactic' in element: - te['tactic'] = element['tactic'] - temp = self._grabDict(te, corpus) - listing = {} - for k in temp.keys(): - if temp[k] != {}: - listing[k] = temp[k].get_dict() - else: - listing[k] = {} - for elm in listing: - if name in listing[elm]: - values[elm] = listing[elm][name] - else: - if defaults is not None: - if name in defaults: - values[elm] = defaults[name] - continue - values[elm] = self._default_values[name] - try: - return lda(values) - except IndexError and KeyError: - print('Unable to continue, lambda targeting "{}" could not operate' - ' correctly on {}. Maybe the field is missing?' - .format(name, element)) - print('[RAW] Extracted matching elements: {}'.format(listing)) - raise BadLambda diff --git a/requirements.txt b/requirements.txt index 7f860e86..a9b12dec 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,31 +1,15 @@ -antlr4-python3-runtime==4.7.2 -cairocffi==1.1.0 -CairoSVG==2.4.2 -certifi==2018.11.29 -cffi==1.14.1 -chardet==3.0.4 -colour==0.1.5 -cssselect2==0.3.0 -defusedxml==0.6.0 -drawSvg==1.6.0 -et-xmlfile==1.0.1 -idna==2.8 -imageio==2.9.0 -jdcal==1.4.1 -numpy==1.19.1 -openpyxl==3.0.3 -Pillow==7.2.0 -pycparser==2.20 -python-dateutil==2.8.0 -pytz==2018.9 -requests==2.21.0 -simplejson==3.16.0 +antlr4-python3-runtime==4.8 +certifi==2020.12.5 +chardet==4.0.0 +idna==2.10 +python-dateutil==2.8.1 +pytz==2021.1 +requests==2.25.1 +simplejson==3.17.2 six==1.15.0 -stix2==2.0.2 -stix2-patterns==1.1.0 -tabulate==0.8.3 -taxii2-client==2.2.1 -tinycss2==1.0.2 -tqdm==4.31.1 -urllib3==1.24.2 -webencodings==0.5.1 \ No newline at end of file +stix2==2.1.0 +stix2-patterns==1.3.2 +tabulate==0.8.9 +taxii2-client==2.3.0 +tqdm==4.60.0 +urllib3==1.26.4