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

Commit d93fb1c

Browse files
author
clittle
committed
Cleaned things up slightly, improved documentation
1 parent 6a00586 commit d93fb1c

3 files changed

Lines changed: 100 additions & 61 deletions

File tree

layers/README.md

Lines changed: 82 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,73 @@
11
# layers
22

3-
This folder contains modules and scripts for working with ATT&CK layers. The core module allows users to load, validate, process, 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).
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).
44

5+
#### Core Modules
56
| script | description |
67
|:-------|:------------|
7-
| [exceptions.py](core/exceptions.py) | Implements errors and supporting functions for the core module. |
8-
| [filter.py](core/filter.py) | Implements a basic filter object. |
9-
| [gradient.py](core/gradient.py) | Implements a basic gradient object. |
10-
| [layer.py](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-
| [layerobj.py](core/layerobj.py) | Implements a basic layer object. This should not be manipulated directly, instead, use the [layer representation](core/layer.py). |
12-
| [layout.py](core/layout.py) | Implements a basic layout object. |
13-
| [legenditem.py](core/legenditem.py) | Implements a basic legenditem object. |
14-
| [metadata.py](core/metadata.py) | Implements a basic metadata object. |
15-
| [technique.py](core/technique.py) | Implements a basic technique object. |
16-
| [layerops.py](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. |
17-
18-
## layer.py
19-
Layer.py provides a Layer class, which is the interface used to interact with the rest of the core module, and provides an abstract representation of an ATT&CK layer. The layer class has a collection of methods, the functionality of which is documented in the table below. An example of how to interface with the class to load and retrieve layer objects is documented following that.
20-
21-
| method | description |
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 |
2225
|:-------|:------------|
23-
| Layer().load_input(_input_) | Loads an ATT&CK layer from either a dictionary or a string representation of a dictionary. |
24-
| Layer().load_file(_input_) | Loads an ATT&CK layer from a file location specified by the _input_. |
25-
| Layer().export_file(_input_) | Saves the current state of the loaded ATT&CK layer to a json file denoted by the _input_. |
26-
| Layer().get_dict() | Returns a representation of the current ATT&CK layer object as a dictionary. |
26+
| x.load_input(_input_) | Loads an ATT&CK layer from either a dictionary or a string representation of a dictionary. |
27+
| x.load_file(_input_) | Loads an ATT&CK layer from a file location specified by the _input_. |
28+
| x.export_file(_input_) | Saves the current state of the loaded ATT&CK layer to a json file denoted by the _input_. |
29+
| x.get_dict() | Returns a representation of the current ATT&CK layer object as a dictionary. |
2730

2831
#### Example Usage
2932

3033
```python
3134
example_layer_dict = {
3235
"name": "example layer",
3336
"version": "3.0",
34-
"domain": "mitre-enterprise",
35-
"description": "hello, world",
36-
...
37-
}
37+
"domain": "mitre-enterprise"
38+
}
39+
3840
example_layer_location = "/path/to/layer/file.json"
3941
example_layer_out_location = "/path/to/new/layer/file.json"
4042

41-
from layers.core.layer import Layer
43+
from layers.core import Layer
4244

43-
layer1 = Layer(example_layer_dict)
44-
layer1.export_file(example_layer_out_location)
45+
layer1 = Layer(example_layer_dict) # Create a new layer and load existing data
46+
layer1.export_file(example_layer_out_location) # Write out the loaded layer to the specified file
4547

46-
layer2 = Layer()
47-
layer2.load_input(example_layer_dict)
48-
layer2.get_dict()
48+
layer2 = Layer() # Create a new layer object
49+
layer2.load_input(example_layer_dict) # Load layer data into existing layer object
50+
print(layer2.get_dict()) # Retrieve the loaded layer's data as a dictionary, and print it
4951

50-
layer3 = Layer()
51-
layer3.load_file(example_layer_location)
52+
layer3 = Layer() # Create a new layer object
53+
layer3.load_file(example_layer_location) # Load layer data from a file into existing layer object
5254
```
5355

5456
## layerops.py
5557
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.
5658

57-
| method | description |
58-
|:-------|:------------|
59-
| 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 elements of the combined layers are missing them. |
60-
| LayerOps.process(_data_, defaults=_defaults_) | 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. |
59+
##### LayerOps()
60+
```python
61+
x = LayerOps(score=score, comment=comment, enabled=enabled, colors=colors, metadata=metadata, name=name, desc=desc, default_values=default_values)
62+
```
63+
64+
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.
65+
66+
##### .process() Method
67+
```python
68+
x.process(data, defaults=defaults)
69+
```
70+
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.
6171

6272
#### Example Usage
6373
```python
@@ -67,19 +77,42 @@ from layers.core.layer import Layer
6777
demo = Layer()
6878
demo.load_file("C:\Users\attack\Downloads\layer.json")
6979
demo2 = Layer()
70-
demo2.load_input({"name": "example layer", ... })
80+
demo2.load_file("C:\Users\attack\Downloads\layer2.json")
81+
demo3 = Layer()
82+
demo3.load_file("C:\Users\attack\Downloads\layer3.json")
7183

72-
lo = LayerOps(score=lambda x: x[0] * x[1],
84+
# Example 1) Build a LayerOps object that takes a list and averages scores across the layers
85+
lo = LayerOps(score=lambda x: sum(x) / len(x),
7386
name=lambda x: x[1],
74-
desc=lambda x: "This is an list example")
75-
out_layer = lo.process([demo, demo2])
76-
out_layer.export_file("C:\demo_layer1.json")
77-
78-
lo2 = LayerOps(score=lambda x: x['a'],
87+
desc=lambda x: "This is an list example") # Build LayerOps object
88+
out_layer = lo.process([demo, demo2]) # Trigger processing on a list of demo and demo2 layers
89+
out_layer.export_file("C:\demo_layer1.json") # Save averaged layer to file
90+
out_layer2 = lo.process([demo, demo2, demo3]) # Trigger processing on a list of demo, demo2, demo3
91+
visual_aid = out_layer2.get_dict("C:\demo_layer2.json") # Retrieve dictionary representation of processed layer
92+
93+
# Example 2) Build a LayerOps object that takes a dictionary and averages scores across the layers
94+
lo2 = LayerOps(score=lambda x: sum([x[y] for y in x]) / len([x[y] for y in x]),
7995
color=lambda x: x['b'],
80-
desc=lambda x: "This is a dict example")
81-
out_layer2 = lo2.process({'a': demo, 'b': demo2})
82-
dict_layer = out_layer2.get_dict()
83-
print(dict_layer)
84-
out_layer2.export_file("C:\demo_layer2.json")
85-
```
96+
desc=lambda x: "This is a dict example") # Build LayerOps object, with lambda
97+
out_layer3 = lo2.process({'a': demo, 'b': demo2}) # Trigger processing on a dictionary of demo and demo2
98+
dict_layer = out_layer3.get_dict() # Retrieve dictionary representation of processed layer
99+
print(dict_layer) # Display retrieved dictionary
100+
out_layer4 = lo2.process({'a': demo, 'b': demo2, 'c': demo3})# Trigger processing on a dictionary of demo, demo2, demo3
101+
out_layer4.export_file("C:\demo_layer4.json") # Save averaged layer to file
102+
103+
# Example 3) Build a LayerOps object that takes a single element dictionary and inverts the score
104+
lo3 = LayerOps(score=lambda x: 100 - x['a'],
105+
desc= lambda x: "This is a simple example") # Build LayerOps object to invert score (0-100 scale)
106+
out_layer5 = lo3.process({'a': demo}) # Trigger processing on dictionary of demo
107+
print(out_layer5.get_dict()) # Display processed layer in dictionary form
108+
out_layer5.export_file("C:\demo_layer5.json") # Save inverted score layer to file
109+
110+
# Example 4) Build a LayerOps object that combines the comments from elements in the list, with custom defaults
111+
lo4 = LayerOps(score=lambda x: '; '.join(x),
112+
default_values= {
113+
"comment": "This was an example of new default values"
114+
},
115+
desc= lambda x: "This is a defaults example") # Build LayerOps object to combine descriptions, defaults
116+
out_layer6 = lo4.process([demo2, demo3]) # Trigger processing on a list of demo2 and demo0
117+
out_layer6.export_file("C:\demo_layer6.json") # Save combined comment layer to file
118+
```

layers/core/technique.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from layers.core.exceptions import BadInput, handler, typeChecker, \
2-
UNSETVALUE, UnknownTechniqueProperty
2+
UNSETVALUE, UnknownTechniqueProperty, BadType
33
from layers.core.metadata import Metadata
44

55

@@ -70,8 +70,12 @@ def score(self):
7070

7171
@score.setter
7272
def score(self, score):
73-
typeChecker(type(self).__name__, score, int, "score")
74-
self.__score = score
73+
try:
74+
typeChecker(type(self).__name__, score, int, "score")
75+
self.__score = score
76+
except BadType:
77+
typeChecker(type(self).__name__, score, float, "score")
78+
self.__score = int(score)
7579

7680
@property
7781
def color(self):

layers/manipulators/layerops.py

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
# out_layer2.export_file("C:\demo_layer2.json")
2121

2222
import copy
23-
from layers.core.layer import Layer
23+
from layers.core import Layer
2424

2525

2626
class InvalidFormat(Exception):
@@ -57,14 +57,16 @@ def __init__(self, score=None, comment=None, enabled=None, colors=None,
5757
self.metadata = metadata
5858
self.name = name
5959
self.desc = desc
60-
if default_values is None:
61-
self.default_values = {
62-
"comment": "",
63-
"enabled": True,
64-
"color": "#ffffff",
65-
"score": 1,
66-
"metadata": []
67-
}
60+
self.default_values = {
61+
"comment": "",
62+
"enabled": True,
63+
"color": "#ffffff",
64+
"score": 1,
65+
"metadata": []
66+
}
67+
if default_values is not None:
68+
for entry in default_values:
69+
self.default_values[entry] = default_values[entry]
6870

6971
def process(self, data, defaults=None):
7072
"""

0 commit comments

Comments
 (0)