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

Commit 1562cf3

Browse files
author
clittle
committed
Bugfixes and tweaks from review comments
1 parent 76cf166 commit 1562cf3

7 files changed

Lines changed: 272 additions & 113 deletions

File tree

layers/README.md

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ tells the exporter what data source to use when building the output matrix. Vali
147147
x = ToExcel(domain='enterprise', source='taxii', local=None)
148148
```
149149
The ToExcel constructor takes domain, server, and local arguments during instantiation. The domain can
150-
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.
150+
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.
151151

152152
##### .to_xlsx() Method
153153
```python
@@ -172,14 +172,14 @@ t2.to_xlsx(layer=lay, filepath="demo2.xlsx")
172172
```
173173

174174
## 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
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
176176
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.
177177

178-
##### ToSVG()
178+
##### ToSvg()
179179
```python
180180
x = ToSvg(domain='enterprise', source='taxii', local=None, config=None)
181181
```
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. 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.
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. 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.
183183

184184
##### SVGConfig()
185185
```python
@@ -190,25 +190,25 @@ y = SVGConfig(width=8.5, height=11, headerHeight=1, unit="in", showSubtechniques
190190
```
191191
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.
192192

193-
| attribute| description |
194-
|:-------|:------------|
195-
| width | Desired SVG width |
196-
| height | Desired SVG height |
197-
| headerHeight | Desired Header Block height |
198-
| unit | SVG measurement units (qualifies width, height, etc.) |
199-
| showSubtechniques | Display form for subtechniques - "all", "expanded" (decided by layer), or "none" |
200-
| font | What font style to use - "sans", "sans-serif", or "monospace" |
201-
| tableBorderColor | Hex color to use for the technique borders |
202-
| showHeader | Whether or not to show Header Blocks |
203-
| legendDocked | Whether or not the legend should be docked |
204-
| legendX | Where to place the legend on the x axis if not docked |
205-
| legendY | Where to place the legend on the y axis if not docked |
206-
| legendWidth | Width of the legend if not docked |
207-
| legendHeight | Height of the legend if not docked |
208-
| showLegend | Whether or not to show the legend |
209-
| showFilters | Whether or not to show the Filter Header Block |
210-
| showAbout | Whether or not to show the About Header Block |
211-
| border | What default border width to use |
193+
| attribute| description | type | default value |
194+
|:-------|:------------|:------------|:------------|
195+
| width | Desired SVG width | number | 8.5 |
196+
| height | Desired SVG height | number | 11 |
197+
| headerHeight | Desired Header Block height | number | 1 |
198+
| unit | SVG measurement units (qualifies width, height, etc.) - "in", "cm", "px", "em", or "pt"| string | "in" |
199+
| showSubtechniques | Display form for subtechniques - "all", "expanded" (decided by layer), or "none" | string | "expanded" |
200+
| font | What font style to use - "serif", "sans-serif", or "monospace" | string | "sans-serif" |
201+
| tableBorderColor | Hex color to use for the technique borders | string | "#6B7279" |
202+
| showHeader | Whether or not to show Header Blocks | bool | True |
203+
| legendDocked | Whether or not the legend should be docked | bool | True |
204+
| legendX | Where to place the legend on the x axis if not docked | number | 0 |
205+
| legendY | Where to place the legend on the y axis if not docked | number | 1 |
206+
| legendWidth | Width of the legend if not docked | number | 2 |
207+
| legendHeight | Height of the legend if not docked | number | 1 |
208+
| showLegend | Whether or not to show the legend | bool | True |
209+
| showFilters | Whether or not to show the Filter Header Block | bool | True |
210+
| showAbout | Whether or not to show the About Header Block | bool | True |
211+
| border | What default border width to use | number | 0.104 |
212212

213213
##### .to_svg() Method
214214
```python

layers/exporters/matrix_gen.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,8 @@ def _get_technique_listing(self, tactic, domain='enterprise'):
139139
subtechs = {}
140140
techs = self.collections[domain].query([Filter('type', '=', 'attack-pattern'), Filter('kill_chain_phases.phase_name', '=', tactic)])
141141
for entry in techs:
142-
if entry['kill_chain_phases'][0]['kill_chain_name'] == 'mitre-attack':
142+
if entry['kill_chain_phases'][0]['kill_chain_name'] == 'mitre-attack' or \
143+
entry['kill_chain_phases'][0]['kill_chain_name'] == 'mitre-mobile-attack':
143144
tid = [t['external_id'] for t in entry['external_references'] if 'attack' in t['source_name']]
144145
if '.' not in tid[0]:
145146
techniques.append(MatrixEntry(id=tid[0], name=entry['name']))

layers/exporters/svg_objects.py

Lines changed: 55 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@
33
import os
44
from PIL import ImageFont
55

6+
try:
7+
from core.gradient import Gradient
8+
except ModuleNotFoundError:
9+
from ..core.gradient import Gradient
10+
11+
612
def convertToPx(quantity, unit):
713
"""
814
INTERNAL: Convert values to pixels
@@ -37,6 +43,20 @@ def _getstringwidth(string, font, size):
3743
length, _ = font.getsize(string)
3844
return length
3945

46+
def _getstringheight(string, font, size):
47+
"""
48+
INTERNAL: Calculate the width of a string (in pixels)
49+
50+
:param string: string to evaluate
51+
:param font: font to use
52+
:param size: font size
53+
:return: pixel height of string
54+
"""
55+
font = ImageFont.truetype('{}/fonts/{}.ttf'.format(os.path.sep.join(__file__.split(os.path.sep)[:-1]), font),
56+
int(size))
57+
_, height = font.getsize(string)
58+
return height
59+
4060
def _findSpace(words, width, height, maxFontSize):
4161
"""
4262
INTERNAL: Find space locations for a string to keep it within width x height
@@ -57,8 +77,10 @@ def _findSpace(words, width, height, maxFontSize):
5777
for w in range(0, len(words)):
5878
word = words[w]
5979
longestWordLength = max(longestWordLength, len(word))
60-
61-
fitTextWidth = ((width - (2 * padding)) / longestWordLength * 1.45)
80+
try:
81+
fitTextWidth = ((width - (2 * padding)) / longestWordLength * 1.45)
82+
except:
83+
pass
6284
size = min(maxFontSize, fitTextHeight, fitTextWidth)
6385
return size
6486

@@ -101,6 +123,7 @@ def _optimalFontSize(st, width, height, maxFontSize=12):
101123
class Cell(draw.DrawingParentElement):
102124
TAG_NAME = 'rect'
103125
def __init__(self, height, width, fill, tBC, ctype=None):
126+
# tBC = tableBorderColor, ctype='class' field on resulting svg object, fill=[R,G,B]
104127
super().__init__(height=height, width=width, style='fill: rgb({}, {}, {})'.format(fill[0], fill[1], fill[2]),
105128
stroke=tBC)
106129
if ctype:
@@ -109,6 +132,7 @@ def __init__(self, height, width, fill, tBC, ctype=None):
109132
class HeaderRect(draw.DrawingParentElement):
110133
TAG_NAME = 'rect'
111134
def __init__(self, width, height, ctype, x=None, y=None, outline=True):
135+
# ctype='class' field on resulting svg object, x=x coord, y=y coord
112136
super().__init__(width=width, height=height, fill='white', rx='5')
113137
self.args['class'] = ctype
114138
if x:
@@ -121,6 +145,7 @@ def __init__(self, width, height, ctype, x=None, y=None, outline=True):
121145
class G(draw.DrawingParentElement):
122146
TAG_NAME = 'g'
123147
def __init__(self, tx=None, ty=None, style=None, ctype=None):
148+
# tx=translate x, ty=translate y, ctype='class' field on resulting svg object
124149
super().__init__()
125150
if tx is None:
126151
tx = 0
@@ -135,10 +160,12 @@ def __init__(self, tx=None, ty=None, style=None, ctype=None):
135160
class Line(draw.DrawingParentElement):
136161
TAG_NAME = 'line'
137162
def __init__(self, x1, x2, y1, y2, stroke):
163+
# x1=start x, x2=stop x, y1=start y, y2=stop y, stroke='stroke' field on resulting svg object
138164
super().__init__(x1=x1, x2=x2, y1=y1, y2=y2, stroke=stroke)
139165

140166
class Text(draw.Text):
141167
def __init__(self, text, font_size, ctype, position=None, tx=None, ty=None, x=None, y=None, fill=None):
168+
# ctype='class' object on resulting svg, position='text-anchor' field, tx/ty=translate x/y, x/y=x/y coord
142169
if x is None:
143170
x = 0
144171
if y is None:
@@ -159,6 +186,7 @@ def __init__(self, text, font_size, ctype, position=None, tx=None, ty=None, x=No
159186
class Swatch(draw.DrawingParentElement):
160187
TAG_NAME = 'rect'
161188
def __init__(self, height, width, fill):
189+
# fill= [R,G,B]
162190
super().__init__(height=height, width=width, style='fill: rgb({}, {}, {})'.format(fill[0], fill[1], fill[2]))
163191

164192

@@ -181,10 +209,10 @@ def build(height, width, label, config, variant='text', t1text=None, t2text=None
181209
g = G(ty=5)
182210
rect = HeaderRect(width, height, 'header-box')
183211
g.append(rect)
184-
rect2 = HeaderRect(_getstringwidth(label, config.font, 12), height/5, 'label-cover', x=8, y=-9.67,
185-
outline=False)
212+
rect2 = HeaderRect(_getstringwidth(label, config.font, 12), _getstringheight(label, config.font, 12),
213+
'label-cover', x=7, y=-5, outline=False)
186214
g.append(rect2)
187-
text = Text(label, 12, 'header-box-label', x=10, y=3)
215+
text = Text(label, 12, 'header-box-label', x=8, y=3)
188216
g.append(text)
189217
internal = G(tx=5, ctype='header-box-content')
190218
g.append(internal)
@@ -193,18 +221,25 @@ def build(height, width, label, config, variant='text', t1text=None, t2text=None
193221
internal.append(upper)
194222
if t1text is not None:
195223
fs, patch_text = _optimalFontSize(t1text, width, (height-80/2), maxFontSize=28)
196-
t1 = Text("\n".join(patch_text), fs, '', x=4, y=20)
224+
lines = len(patch_text)
225+
y = (height-5)/4 + 2.1
226+
if lines > 1:
227+
y = y - (((height-5)/16) * (lines-1))
228+
t1 = Text("\n".join(patch_text), fs, '', x=4, y=y)
197229
upper.append(t1)
198230
upper.append(Line(0, width-10, (height-5)/2, (height-5)/2, stroke='#dddddd'))
199-
if t2text is not None:
231+
if t2text is not None and t2text is not "":
200232
upper_fs = fs
201-
lower = G(tx=0, ty= ((height-5)/2 + 20))
233+
lower_offset = ((height-5)/2 + 2.1)
234+
lower = G(tx=0, ty= lower_offset)
202235
fs, patch_text = _optimalFontSize(t2text, width, (height - (height/3 + upper_fs)), maxFontSize=28)
203-
y = 10
236+
y = (height-5)/4 + 2.1
204237
lines = len(patch_text)
205238
adju = "\n".join(patch_text)
206239
if lines > 1:
207-
y = y / (4 ** lines)
240+
y = y - (((height - 5) / 16) * (lines - 1))
241+
if float(fs) > lower_offset:
242+
y = y + 2*(float(fs) - lower_offset)
208243
t2 = Text(adju, fs, '', x=4, y=y)
209244
lower.append(t2)
210245
internal.append(lower)
@@ -237,6 +272,8 @@ def build(height, width, label, config, variant='text', t1text=None, t2text=None
237272
class SVG_Technique:
238273
def __init__(self, gradient):
239274
self.grade = gradient
275+
if self.grade == None:
276+
self.grade = Gradient(colors=["#ff6666", "#ffe766", "#8ec843"], minValue=1, maxValue=100)
240277

241278
def build(self, offset, technique, height, width, tBC, subtechniques=[], mode=(True, False), tactic=None,
242279
colors=[]):
@@ -254,7 +291,6 @@ def build(self, offset, technique, height, width, tBC, subtechniques=[], mode=(T
254291
:param colors: List of all default color values if no score can be found
255292
:return: The newly created SVG technique block
256293
"""
257-
indent = 11.2
258294
g = G(ty=offset)
259295
c = self._com_color(technique, tactic, colors)
260296
t = dict(name=self._disp(technique.name, technique.id, mode), id=technique.id,
@@ -264,21 +300,21 @@ def build(self, offset, technique, height, width, tBC, subtechniques=[], mode=(T
264300
g.append(text)
265301
new_offset = height
266302
for entry in subtechniques:
267-
gp = G(tx=indent, ty=new_offset)
303+
gp = G(tx=width/5, ty=new_offset)
268304
g.append(gp)
269305
c = self._com_color(entry, tactic, colors)
270306
st = dict(name=self._disp(entry.name, entry.id, mode), id=entry.id,
271307
color=tuple(int(c[i:i + 2], 16) for i in (0, 2, 4)))
272-
subtech, subtext = self._block(st, height, width - indent, tBC=tBC)
308+
subtech, subtext = self._block(st, height, width - width/5, tBC=tBC)
273309
gp.append(subtech)
274310
gp.append(subtext)
275311
new_offset = new_offset + height
276312
if len(subtechniques):
277-
g.append(draw.Lines(2, -height,
278-
9, -height * 2,
279-
9, -height * (len(subtechniques) + 1),
280-
indent, -height * (len(subtechniques) + 1),
281-
indent, -height,
313+
g.append(draw.Lines(width/16, -height,
314+
width/8, -height * 2,
315+
width/8, -height * (len(subtechniques) + 1),
316+
width/5, -height * (len(subtechniques) + 1),
317+
width/5, -height,
282318
close=True,
283319
fill=tBC,
284320
stroke=tBC))
@@ -304,7 +340,7 @@ def _block(technique, height, width, tBC):
304340

305341
y = height / 2
306342
if lines > 0:
307-
y = y - y / (2**lines) + fs / (lines + 1)
343+
y = (height - (lines * fs)) / 2 + height/10 #padding
308344
else:
309345
y = y + fs / 4
310346

@@ -332,9 +368,6 @@ def _com_color(self, technique, tactic, colors=[]):
332368
for x in colors:
333369
if x[0] == technique.id and (x[1] == tactic or not x[1]):
334370
c = x[2][1:]
335-
if c == 'FFFFFF':
336-
if 0 >= self.grade.minValue:
337-
c = self.grade.compute_color(0)[1:]
338371
return c
339372

340373
@staticmethod

0 commit comments

Comments
 (0)