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

Commit f28d665

Browse files
author
clittle
committed
Cleaned up and Documented things
1 parent 4265624 commit f28d665

4 files changed

Lines changed: 204 additions & 64 deletions

File tree

layers/README.md

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,18 @@ This folder contains modules and scripts for working with ATT&CK Navigator layer
2222
| script | description |
2323
|:-------|:------------|
2424
| [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. |
25+
| [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. |
2526
##### Utility Modules
2627
| script | description |
2728
|:-------|:------------|
2829
| [excel_templates](exporters/excel_templates.py) | Provides a means by which to convert a matrix into a clean excel matrix template. |
2930
| [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. |
31+
| [svg_templates](exporters/svg_templates.py) | Provides a means by which to convert a layer file into a marked up svg file. |
32+
| [svg_objects](exporters/svg_objects.py) | Provides raw templates and supporting functionality for generating svg objects. |
33+
##### Command LIne Tools
34+
| script | description |
35+
|:-------|:------------|
36+
| [cmdline.py](cmdline.py) | A commandline utility to export Layer files to excel or svg formats using the exporter tools. Run with `-h` for usage. |
3037

3138
## Layer
3239
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.
@@ -142,7 +149,7 @@ x = ToExcel(domain='enterprise', source='taxii', local=None)
142149
The ToExcel constructor takes domain, server, and local arguments during instantiation. The domain can
143150
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.
144151

145-
##### .to_file() Method
152+
##### .to_xlsx() Method
146153
```python
147154
x.to_xlsx(layer=layer, filepath="layer.xlsx")
148155
```
@@ -162,4 +169,36 @@ t.to_xlsx(layer=lay, filepath="demo.xlsx")
162169
#Using local stix data for template
163170
t2 = ToExcel(domain='mobile', source='local', local='path/to/local/stix.json')
164171
t2.to_xlsx(layer=lay, filepath="demo2.xlsx")
172+
```
173+
174+
## to_svg.py
175+
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
176+
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.
177+
178+
##### ToSVG()
179+
```python
180+
x = ToSVG(domain='enterprise', source='taxii', local=None)
181+
```
182+
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.
183+
184+
##### .to_svg() Method
185+
```python
186+
x.to_svg(layer=layer, filepath="layer.svg")
187+
```
188+
The to_svg method exports the layer file referenced as `layer`, as an excel file to the
189+
`filepath` specified.
190+
191+
#### Example Usage
192+
```python
193+
from layers import Layer
194+
from layers import ToSVG
195+
196+
lay = Layer()
197+
lay.from_file("path/to/layer/file.json")
198+
# Using taxii server for template
199+
t = ToSVG(domain=lay.layer.domain, source='taxii')
200+
t.to_svg(layer=lay, filepath="demo.svg")
201+
#Using local stix data for template
202+
t2 = ToSVG(domain='mobile', source='local', local='path/to/local/stix.json')
203+
t2.to_svg(layer=lay, filepath="demo2.svg")
165204
```

layers/exporters/svg_objects.py

Lines changed: 92 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
import drawSvg as draw
22
import string
3+
import colorsys
34

4-
def getApproximateStringWidth(st):
5+
def _getapproximatestringwidth(st):
6+
"""
7+
Internal - estimate the width of a string... somewhat poorly
8+
9+
:param st: The string to evaluate
10+
:return: A number that roughly describes the length of the string
11+
"""
512
size = 0
613
for s in st:
714
if s in 'lij|\' ':
@@ -22,23 +29,39 @@ def getApproximateStringWidth(st):
2229
size += 50
2330
return size * 6 / 75
2431

25-
def calculateViableStringSize(st, target, width):
32+
def _calculateviablestringsize(st, target, width):
33+
"""
34+
Internal - Calculate the font size needed to make a string fit, roughly
35+
36+
:param st: The string to evaluate
37+
:param target: The initial font size
38+
:param width: The width to fit into
39+
:return: An adjusted font size
40+
"""
2641
current_font_size = target
27-
while (getApproximateStringWidth(st) * current_font_size) > (width * 10):
42+
while (_getapproximatestringwidth(st) * current_font_size) > (width * 10):
2843
current_font_size -= 1
2944
return current_font_size
3045

31-
def wrap(st, font_size, width):
46+
def _wrap(st, font_size, width):
47+
"""
48+
Internal - Wrap a string in the event it can't fit on one line
49+
50+
:param st: String to wrap
51+
:param font_size: Target font size for the string
52+
:param width: The width of the block the line fits in
53+
:return: A string with line breaks to make it fit in the space provided
54+
"""
3255
p1 = 0
3356
p2 = 1
3457
block = (width - 30) * 10
3558
construct = st.split(' ')
3659
if len(construct) == 1:
3760
return st
3861
build = ''
39-
while(p2 < len(construct)):
62+
while p2 < len(construct):
4063
loop = True
41-
strwidth = getApproximateStringWidth(' '.join(construct[p1:p2])) * font_size
64+
strwidth = _getapproximatestringwidth(' '.join(construct[p1:p2])) * font_size
4265

4366
if strwidth < block:
4467
p2 += 1
@@ -55,7 +78,7 @@ def wrap(st, font_size, width):
5578
build += ' '.join(construct[p1:p2]) + '\n'
5679
p1 += 1
5780
p2 += 1
58-
if getApproximateStringWidth(' '.join(construct[p1:])) * font_size > block:
81+
if _getapproximatestringwidth(' '.join(construct[p1:])) * font_size > block:
5982
build += ' '.join(construct[p1:-1]) + '\n' + construct[-1]
6083
else:
6184
build += ' '.join(construct[p1:])
@@ -85,9 +108,9 @@ class G(draw.DrawingParentElement):
85108
TAG_NAME = 'g'
86109
def __init__(self, tx=None, ty=None, style=None, ctype=None):
87110
super().__init__()
88-
if tx == None:
111+
if tx is None:
89112
tx = 0
90-
if ty == None:
113+
if ty is None:
91114
ty = 0
92115
self.args['transform']='translate(' + str(tx) +',' + str(ty) +')'
93116
if style:
@@ -101,21 +124,23 @@ def __init__(self, x1, x2, y1, y2, stroke):
101124
super().__init__(x1=x1, x2=x2, y1=y1, y2=y2, stroke=stroke)
102125

103126
class Text(draw.Text):
104-
def __init__(self, text, font_size, ctype, position=None, tx=None, ty=None, x=None, y=None):
105-
if x == None:
127+
def __init__(self, text, font_size, ctype, position=None, tx=None, ty=None, x=None, y=None, fill=None):
128+
if x is None:
106129
x = 0
107-
if y == None:
130+
if y is None:
108131
y = 0
109132
super().__init__(text=text, fontSize=font_size, x=x, y=-y)
110133
self.args['class'] = ctype
111-
if tx == None:
134+
if tx is None:
112135
tx = 0
113-
if ty == None:
136+
if ty is None:
114137
ty = 0
115138
if tx != 0 or ty != 0:
116139
self.args['transform']='translate(' + str(tx) +',' + str(ty) +')'
117-
if position != None:
140+
if position:
118141
self.args['style'] = 'text-anchor: {}'.format(position)
142+
if fill:
143+
self.args['fill'] = fill
119144

120145
class Root(draw.DrawingParentElement):
121146
TAG_NAME = 'g'
@@ -128,23 +153,39 @@ def __init__(self, height, width, fill):
128153
super().__init__(height=height, width=width, style='fill: rgb({}, {}, {})'.format(fill[0], fill[1], fill[2]))
129154

130155

131-
class SVG_HeaderBlock():
132-
def build(self, height, width, label, type='text', t1text=None, t1size=None, t2text=None, t2size=None, colors=[],
156+
class SVG_HeaderBlock:
157+
@staticmethod
158+
def build(height, width, label, variant='text', t1text=None, t1size=None, t2text=None, t2size=None, colors=[],
133159
values=(0,100)):
160+
"""
161+
Build a single SVG Header Block object
162+
163+
:param height: Height of the block
164+
:param width: Width of the block
165+
:param label: Label for the block
166+
:param variant: text or graphic - the type of header block to build
167+
:param t1text: upper text
168+
:param t1size: upper text size
169+
:param t2text: lower text
170+
:param t2size: lower text size
171+
:param colors: colors for the graphic variant
172+
:param values: values for the graphic variant's gradient
173+
:return:
174+
"""
134175
g = G(ty=5)
135176
rect = HeaderRect(width, height, 'header-box')
136177
g.append(rect)
137-
rect2 = HeaderRect(getApproximateStringWidth(label), height/5, 'label-cover', x=8, y=-9.67, outline=False)
178+
rect2 = HeaderRect(_getapproximatestringwidth(label), height/5, 'label-cover', x=8, y=-9.67, outline=False)
138179
g.append(rect2)
139180
text = Text(label, 12, 'header-box-label', x=10, y=3)
140181
g.append(text)
141182
internal = G(tx=5, ctype='header-box-content')
142183
g.append(internal)
143-
if type == 'text':
184+
if variant == 'text':
144185
upper = G(tx=0, ty=2.1)
145186
internal.append(upper)
146187
if t1text is not None:
147-
t1 = Text(t1text, calculateViableStringSize(t1text, t1size, width), '', x=4, y= height/3)
188+
t1 = Text(t1text, _calculateviablestringsize(t1text, t1size, width), '', x=4, y= height/3)
148189
upper.append(t1)
149190
upper.append(Line(0, width-10, (height-5)/2, (height-5)/2, stroke='#dddddd'))
150191
if t2text is not None:
@@ -153,7 +194,7 @@ def build(self, height, width, label, type='text', t1text=None, t1size=None, t2t
153194
lines = 4
154195
fs = t2size
155196
while lines > 2:
156-
adjusted = wrap(t2text, fs, width + 15)
197+
adjusted = _wrap(t2text, fs, width + 15)
157198
lines = adjusted.count('\n')
158199
if lines > 2:
159200
fs = fs / 2
@@ -192,29 +233,40 @@ def build(self, height, width, label, type='text', t1text=None, t1size=None, t2t
192233
cell.append(label)
193234
return g
194235

195-
class SVG_Technique():
236+
class SVG_Technique:
196237
def __init__(self, gradient):
197238
self.grade = gradient
198239

199240
def build(self, offset, technique, subtechniques=[], mode=(True, False), tactic=None, colors=[]):
241+
"""
242+
Build a SVG Technique block
243+
244+
:param offset: Current offset to build the block at (so it fits in the column)
245+
:param technique: The technique to build a block for
246+
:param subtechniques: List of any visible subtechniques for this technique
247+
:param mode: Display mode (Show Name, Show ID)
248+
:param tactic: The corresponding tactic
249+
:param colors: List of all default color values if no score can be found
250+
:return: The newly created SVG technique block
251+
"""
200252
height = 5.6
201253
width = 80
202254
indent = 11.2
203255
g = G(ty=offset)
204-
c = self.com_color(technique, tactic, colors)
205-
t = dict(name=self.disp(technique.name, technique.id, mode), id=technique.id,
256+
c = self._com_color(technique, tactic, colors)
257+
t = dict(name=self._disp(technique.name, technique.id, mode), id=technique.id,
206258
color=tuple(int(c[i:i+2], 16) for i in (0, 2, 4)))
207-
tech, text = self.block(t, height, width)
259+
tech, text = self._block(t, height, width)
208260
g.append(tech)
209261
g.append(text)
210262
new_offset = height
211263
for entry in subtechniques:
212264
gp = G(tx=indent, ty=new_offset)
213265
g.append(gp)
214-
c = self.com_color(entry, tactic, colors)
215-
st = dict(name=self.disp(entry.name, entry.id, mode), id=entry.id,
266+
c = self._com_color(entry, tactic, colors)
267+
st = dict(name=self._disp(entry.name, entry.id, mode), id=entry.id,
216268
color=tuple(int(c[i:i + 2], 16) for i in (0, 2, 4)))
217-
subtech, subtext = self.block(st, height, width - indent)
269+
subtech, subtext = self._block(st, height, width - indent)
218270
gp.append(subtech)
219271
gp.append(subtext)
220272
new_offset = new_offset + height
@@ -229,19 +281,24 @@ def build(self, offset, technique, subtechniques=[], mode=(True, False), tactic=
229281
stroke='#6B7279'))
230282
return g, offset + new_offset
231283

232-
def block(self, technique, height, width):
284+
@staticmethod
285+
def _block(technique, height, width):
233286
tech = Cell(height, width, technique['color'], ctype=technique['id'])
234287
fs = 4.5
235-
adjusted = wrap(technique['name'], fs/3, width - 13)
288+
adjusted = _wrap(technique['name'], fs/3, width - 13)
236289
lines = adjusted.count('\n')
237290
y = 4.31
238291
if lines > 0:
239-
fs = fs / (2**(lines))
292+
fs = fs / (2**lines)
240293
y = y / (2**lines) + .15 * (2**lines)
241-
text = Text(adjusted.encode('utf-8').decode('ascii', 'backslashreplace'), fs, '', x=4, y=y)
294+
hls = colorsys.rgb_to_hls(technique['color'][0], technique['color'][1], technique['color'][2])
295+
fill = None
296+
if hls[1] < 127.5:
297+
fill = 'white'
298+
text = Text(adjusted.encode('utf-8').decode('ascii', 'backslashreplace'), fs, '', x=4, y=y, fill=fill)
242299
return tech, text
243300

244-
def com_color(self, technique, tactic, colors=[]):
301+
def _com_color(self, technique, tactic, colors=[]):
245302
c = 'FFFFFF'
246303
if technique.score:
247304
c = self.grade.compute_color(technique.score)[1:]
@@ -254,7 +311,8 @@ def com_color(self, technique, tactic, colors=[]):
254311
c = self.grade.compute_color(0)[1:]
255312
return c
256313

257-
def disp(self, name, id, mode):
314+
@staticmethod
315+
def _disp(name, id, mode):
258316
p1 = name
259317
p2 = id
260318
if not mode[0]:

0 commit comments

Comments
 (0)