Skip to content
This repository was archived by the owner on Dec 11, 2023. It is now read-only.

Commit 65a41b4

Browse files
committed
merging in layerops bugfix
2 parents dc7a8a9 + ebf6fa6 commit 65a41b4

File tree

14 files changed

+1510
-2
lines changed

14 files changed

+1510
-2
lines changed

CHANGELOG.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
1-
# Changes staged on develop
1+
# V1.4.1 - 18 May 2020
2+
## New Scripts
3+
- New script [technique_mappings_to_csv.py](technique_mappings_to_csv.py) added to support mapping Techniques with Mitigations, Groups or Software. The output is a CSV file. Added in PR [#23](https://github.com/mitre-attack/attack-scripts/pull/23)
24
## Improvements
35
- Updated [diff_stix.py](scripts/diff_stix.py) with sub-techniques support. See issue [#12](https://github.com/mitre-attack/attack-scripts/issues/12).
4-
- New script [technique_mappings_to_csv.py](technique_mappings_to_csv.py) added to support mapping Techniques with Mitigations, Groups or Software. The output is a CSV file. Added in PR [#23](https://github.com/mitre-attack/attack-scripts/pull/23)
6+
## Fixes
7+
- Fixed bug in LayerOps causing issues with cross-tactic techniques, as well as a bug where a score lambda could affect the outcome of other lambdas.
8+
9+
# V1.4 - 5 May 2020
10+
## New Scripts
11+
- Added Layers folder with utility scripts for working with [ATT&CK Navigator](https://github.com/mitre-attack/attack-navigator) Layers. See the Layers [README](layers/README.md) for more details. See issues [#2](https://github.com/mitre-attack/attack-scripts/issues/2) and [#3](https://github.com/mitre-attack/attack-scripts/issues/3).
512

613
# V1.3 - 8 January 2019
714
## New Scripts

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ Note: this repository is a work in progress. In the coming months we will be add
1313
2. Activate the environment: `source env/bin/activate`
1414
3. Install requirements into the virtual environment: `pip3 install -r requirements.txt`
1515

16+
## Layers
17+
This section of the repository contains a collection of modules and scripts for working with ATT&CK Navigator layers. More information about the contents of this section can be found [here](https://github.com/mitre-attack/attack-scripts/blob/master/layers/README.md).
18+
1619
## Training
1720

1821
[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/mitre-attack/attack-scripts/master)

layers/README.md

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
# layers
2+
3+
This folder contains modules and scripts for working with ATT&CK Navigator layers. ATT&CK Navigator Layers are a set of annotations overlayed on top of the ATT&CK Matrix. For more about ATT&CK Navigator layers, visit the ATT&CK Navigator repository. The core module allows users to load, validate, manipulate, and save ATT&CK layers. A brief overview of the components can be found below. All scripts adhere to the MITRE ATT&CK Navigator Layer file format, [version 3.0](https://github.com/mitre-attack/attack-navigator/blob/develop/layers/LAYERFORMATv3.md).
4+
5+
#### Core Modules
6+
| script | description |
7+
|:-------|:------------|
8+
| [filter](core/filter.py) | Implements a basic [filter object](https://github.com/mitre-attack/attack-navigator/blob/develop/layers/LAYERFORMATv3.md#filter-object-properties). |
9+
| [gradient](core/gradient.py) | Implements a basic [gradient object](https://github.com/mitre-attack/attack-navigator/blob/develop/layers/LAYERFORMATv3.md#gradient-object-properties). |
10+
| [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 below. |
11+
| [layout](core/layout.py) | Implements a basic [layout object](https://github.com/mitre-attack/attack-navigator/blob/develop/layers/LAYERFORMATv3.md#layout-object-properties). |
12+
| [legenditem](core/legenditem.py) | Implements a basic [legenditem object](https://github.com/mitre-attack/attack-navigator/blob/develop/layers/LAYERFORMATv3.md#legenditem-object-properties). |
13+
| [metadata](core/metadata.py) | Implements a basic [metadata object](https://github.com/mitre-attack/attack-navigator/blob/develop/layers/LAYERFORMATv3.md#metadata-object-properties). |
14+
| [technique](core/technique.py) | Implements a basic [technique object](https://github.com/mitre-attack/attack-navigator/blob/develop/layers/LAYERFORMATv3.md#technique-object-properties). |
15+
16+
#### Manipulator Scripts
17+
| script | description |
18+
|:-------|:------------|
19+
| [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 below. |
20+
21+
## Layer
22+
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.
23+
24+
| method [x = Layer()]| description |
25+
|:-------|:------------|
26+
| x.from_str(_input_) | Loads an ATT&CK layer from a string representation of a json layer. |
27+
| x.from_dict(_input_) | Loads an ATT&CK layer from a dictionary. |
28+
| x.from_file(_filepath_) | Loads an ATT&CK layer from a file location specified by the _filepath_. |
29+
| x.to_file(_filepath_) | Saves the current state of the loaded ATT&CK layer to a json file denoted by the _filepath_. |
30+
| x.to_dict() | Returns a representation of the current ATT&CK layer object as a dictionary. |
31+
| x.to_str() | Returns a representation of the current ATT&CK layer object as a string representation of a dictionary. |
32+
33+
#### Example Usage
34+
35+
```python
36+
example_layer_dict = {
37+
"name": "example layer",
38+
"version": "3.0",
39+
"domain": "mitre-enterprise"
40+
}
41+
42+
example_layer_location = "/path/to/layer/file.json"
43+
example_layer_out_location = "/path/to/new/layer/file.json"
44+
45+
from layers.core import Layer
46+
47+
layer1 = Layer(example_layer_dict) # Create a new layer and load existing data
48+
layer1.to_file(example_layer_out_location) # Write out the loaded layer to the specified file
49+
50+
layer2 = Layer() # Create a new layer object
51+
layer2.from_dict(example_layer_dict) # Load layer data into existing layer object
52+
print(layer2.to_dict()) # Retrieve the loaded layer's data as a dictionary, and print it
53+
54+
layer3 = Layer() # Create a new layer object
55+
layer3.from_file(example_layer_location) # Load layer data from a file into existing layer object
56+
```
57+
58+
## layerops.py
59+
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.
60+
61+
##### LayerOps()
62+
```python
63+
x = LayerOps(score=score, comment=comment, enabled=enabled, colors=colors, metadata=metadata, name=name, desc=desc, default_values=default_values)
64+
```
65+
66+
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.
67+
68+
##### .process() Method
69+
```python
70+
x.process(data, default_values=default_values)
71+
```
72+
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
73+
values with new ones for this specific processing operation.
74+
75+
#### Example Usage
76+
```python
77+
from layers.manipulators.layerops import LayerOps
78+
from layers.core.layer import Layer
79+
80+
demo = Layer()
81+
demo.from_file("C:\Users\attack\Downloads\layer.json")
82+
demo2 = Layer()
83+
demo2.from_file("C:\Users\attack\Downloads\layer2.json")
84+
demo3 = Layer()
85+
demo3.from_file("C:\Users\attack\Downloads\layer3.json")
86+
87+
# Example 1) Build a LayerOps object that takes a list and averages scores across the layers
88+
lo = LayerOps(score=lambda x: sum(x) / len(x),
89+
name=lambda x: x[1],
90+
desc=lambda x: "This is an list example") # Build LayerOps object
91+
out_layer = lo.process([demo, demo2]) # Trigger processing on a list of demo and demo2 layers
92+
out_layer.to_file("C:\demo_layer1.json") # Save averaged layer to file
93+
out_layer2 = lo.process([demo, demo2, demo3]) # Trigger processing on a list of demo, demo2, demo3
94+
visual_aid = out_layer2.to_dict() # Retrieve dictionary representation of processed layer
95+
96+
# Example 2) Build a LayerOps object that takes a dictionary and averages scores across the layers
97+
lo2 = LayerOps(score=lambda x: sum([x[y] for y in x]) / len([x[y] for y in x]),
98+
color=lambda x: x['b'],
99+
desc=lambda x: "This is a dict example") # Build LayerOps object, with lambda
100+
out_layer3 = lo2.process({'a': demo, 'b': demo2}) # Trigger processing on a dictionary of demo and demo2
101+
dict_layer = out_layer3.to_dict() # Retrieve dictionary representation of processed layer
102+
print(dict_layer) # Display retrieved dictionary
103+
out_layer4 = lo2.process({'a': demo, 'b': demo2, 'c': demo3})# Trigger processing on a dictionary of demo, demo2, demo3
104+
out_layer4.to_file("C:\demo_layer4.json") # Save averaged layer to file
105+
106+
# Example 3) Build a LayerOps object that takes a single element dictionary and inverts the score
107+
lo3 = LayerOps(score=lambda x: 100 - x['a'],
108+
desc= lambda x: "This is a simple example") # Build LayerOps object to invert score (0-100 scale)
109+
out_layer5 = lo3.process({'a': demo}) # Trigger processing on dictionary of demo
110+
print(out_layer5.to_dict()) # Display processed layer in dictionary form
111+
out_layer5.to_file("C:\demo_layer5.json") # Save inverted score layer to file
112+
113+
# Example 4) Build a LayerOps object that combines the comments from elements in the list, with custom defaults
114+
lo4 = LayerOps(score=lambda x: '; '.join(x),
115+
default_values= {
116+
"comment": "This was an example of new default values"
117+
},
118+
desc= lambda x: "This is a defaults example") # Build LayerOps object to combine descriptions, defaults
119+
out_layer6 = lo4.process([demo2, demo3]) # Trigger processing on a list of demo2 and demo0
120+
out_layer6.to_file("C:\demo_layer6.json") # Save combined comment layer to file
121+
```

layers/core/__init__.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from .layer import Layer
2+
from .exceptions import *
3+
from .filter import Filter
4+
from .gradient import Gradient
5+
from .layerobj import _LayerObj
6+
from .layout import Layout
7+
from .legenditem import LegendItem
8+
from .metadata import Metadata
9+
from .technique import Technique
10+

layers/core/exceptions.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
UNSETVALUE = '(x)'
2+
3+
4+
class BadInput(Exception):
5+
pass
6+
7+
8+
class BadType(Exception):
9+
pass
10+
11+
12+
class UninitializedLayer(Exception):
13+
pass
14+
15+
16+
class UnknownLayerProperty(Exception):
17+
pass
18+
19+
20+
class UnknownTechniqueProperty(Exception):
21+
pass
22+
23+
24+
def handler(caller, msg):
25+
"""
26+
Prints a debug/warning/error message
27+
:param caller: the entity that called this function
28+
:param msg: the message to log
29+
"""
30+
print('[{}] - {}'.format(caller, msg))
31+
32+
33+
def typeChecker(caller, testee, type, field):
34+
"""
35+
Verifies that the tested object is of the correct type
36+
:param caller: the entity that called this function (used for error
37+
messages)
38+
:param testee: the element to test
39+
:param type: the type the element should be
40+
:param field: what the element is to be used as (used for error
41+
messages)
42+
:raises BadType: error denoting the testee element is not of the
43+
correct type
44+
"""
45+
if not isinstance(testee, type):
46+
handler(caller, '{} [{}] is not a {}'.format(testee, field,
47+
str(type)))
48+
raise BadType
49+
50+
51+
def typeCheckerArray(caller, testee, type, field):
52+
"""
53+
Verifies that the tested object is an array of the correct type
54+
:param caller: the entity that called this function (used for error
55+
messages)
56+
:param testee: the element to test
57+
:param type: the type the element should be
58+
:param field: what the element is to be used as (used for error
59+
messages)
60+
:raises BadType: error denoting the testee element is not of the
61+
correct type
62+
"""
63+
if not isinstance(testee, list):
64+
handler(caller, '{} [{}] is not a {}'.format(testee, field,
65+
"Array"))
66+
raise BadType
67+
if not isinstance(testee[0], type):
68+
handler(caller, '{} [{}] is not a {}'.format(testee, field,
69+
"Array of " + type))
70+
raise BadType
71+
72+
73+
def categoryChecker(caller, testee, valid, field):
74+
"""
75+
Verifies that the tested object is one of a set of valid values
76+
:param caller: the entity that called this function (used for error
77+
messages)
78+
:param testee: the element to test
79+
:param valid: a list of valid values for the testee
80+
:param field: what the element is to be used as (used for error
81+
messages)
82+
:raises BadInput: error denoting the testee element is not one of
83+
the valid options
84+
"""
85+
if testee not in valid:
86+
handler(caller, '{} not a valid value for {}'.format(testee, field))
87+
raise BadInput

layers/core/filter.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
try:
2+
from ..core.exceptions import typeCheckerArray, categoryChecker, \
3+
UNSETVALUE
4+
except ValueError:
5+
from core.exceptions import typeCheckerArray, categoryChecker, \
6+
UNSETVALUE
7+
8+
9+
class Filter:
10+
def __init__(self, domain="mitre-enterprise"):
11+
"""
12+
Initialization - Creates a filter object, with an optional
13+
domain input
14+
15+
:param domain: The domain used for this layer (mitre-enterprise
16+
or mitre-mobile)
17+
"""
18+
self.__stages = UNSETVALUE
19+
self.domain = domain
20+
self.__platforms = UNSETVALUE
21+
22+
@property
23+
def stages(self):
24+
if self.__stages != UNSETVALUE:
25+
return self.__stages
26+
27+
@stages.setter
28+
def stages(self, stage):
29+
typeCheckerArray(type(self).__name__, stage, str, "stage")
30+
categoryChecker(type(self).__name__, stage[0], ["act", "prepare"],
31+
"stages")
32+
self.__stages = stage
33+
34+
@property
35+
def platforms(self):
36+
if self.__platforms != UNSETVALUE:
37+
return self.__platforms
38+
39+
@platforms.setter
40+
def platforms(self, platforms):
41+
typeCheckerArray(type(self).__name__, platforms, str, "platforms")
42+
self.__platforms = []
43+
valids = ["Windows", "Linux", "macOS", "AWS", "GCP", "Azure",
44+
"Azure AD", "Office 365", "SaaS"]
45+
if self.domain == "mitre-mobile":
46+
valids = ['Android', 'iOS']
47+
for entry in platforms:
48+
categoryChecker(type(self).__name__, entry, valids, "platforms")
49+
self.__platforms.append(entry)
50+
51+
def get_dict(self):
52+
"""
53+
Converts the currently loaded data into a dict
54+
:returns: A dict representation of the local filter object
55+
"""
56+
temp = dict()
57+
listing = vars(self)
58+
for entry in listing:
59+
if entry == 'domain':
60+
continue
61+
if listing[entry] != UNSETVALUE:
62+
temp[entry.split(type(self).__name__ + '__')[-1]] \
63+
= listing[entry]
64+
if len(temp) > 0:
65+
return temp

layers/core/gradient.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
try:
2+
from ..core.exceptions import typeChecker, typeCheckerArray
3+
except ValueError:
4+
from core.exceptions import typeChecker, typeCheckerArray
5+
6+
7+
class Gradient:
8+
def __init__(self, colors, minValue, maxValue):
9+
"""
10+
Initialization - Creates a gradient object
11+
12+
:param colors: The array of color codes for this gradient
13+
:param minValue: The minValue for this gradient
14+
:param maxValue: The maxValue for this gradient
15+
"""
16+
self.colors = colors
17+
self.minValue = minValue
18+
self.maxValue = maxValue
19+
20+
@property
21+
def colors(self):
22+
return self.__colors
23+
24+
@colors.setter
25+
def colors(self, colors):
26+
typeCheckerArray(type(self).__name__, colors, str, "colors")
27+
self.__colors = []
28+
for entry in colors:
29+
self.__colors.append(entry)
30+
31+
@property
32+
def minValue(self):
33+
return self.__minValue
34+
35+
@minValue.setter
36+
def minValue(self, minValue):
37+
typeChecker(type(self).__name__, minValue, int, "minValue")
38+
self.__minValue = minValue
39+
40+
@property
41+
def maxValue(self):
42+
return self.__maxValue
43+
44+
@maxValue.setter
45+
def maxValue(self, maxValue):
46+
typeChecker(type(self).__name__, maxValue, int, "maxValue")
47+
self.__maxValue = maxValue
48+
49+
def get_dict(self):
50+
"""
51+
Converts the currently loaded gradient file into a dict
52+
:returns: A dict representation of the current gradient object
53+
"""
54+
return dict(colors=self.__colors, minValue=self.__minValue,
55+
maxValue=self.maxValue)

0 commit comments

Comments
 (0)