Skip to content

Commit b0c3eae

Browse files
Merge pull request #35 from paterva/feat/local-mtz
Feature: mtz config generation for local transforms
2 parents 4022240 + 4d8d005 commit b0c3eae

11 files changed

+836
-221
lines changed

README.md

+33
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
[![Sonatype Jake](https://github.com/paterva/maltego-trx/actions/workflows/sonatype-jack.yml/badge.svg)](https://github.com/paterva/maltego-trx/actions/workflows/sonatype-jack.yml)
66

77
## Release Notes
8+
9+
__1.6.0__: Automatically generate am `.mtz` for your local transforms
10+
811
__1.5.2__: Add logging output for invalid / missing params in xml serialization
912

1013
__1.5.1__: Add ignored files to starter and use README for pypi
@@ -305,6 +308,36 @@ registry.write_settings_config()
305308
handle_run(__name__, sys.argv, application)
306309
```
307310

311+
### Generating an `.mtz` config with your local Transforms
312+
313+
Since `maltego-trx>=1.6.0` you can generate an `.mtz` config file with your local transforms.
314+
315+
If you're already using the `TransformRegistry`, just invoke the `write_local_config()` method.
316+
317+
```python
318+
# project.py
319+
320+
registry.write_local_mtz()
321+
```
322+
323+
This will create a file called `local.mtz` in the current directory. You can then import this file into Maltego and
324+
start using your local transforms faster. Just remember that settings are not passed to local transforms.
325+
326+
The method takes in the same arguments as the interface in the Maltego client.
327+
If you are using a `virtualenv` environment, you might want to change the `command` argument to use that.
328+
329+
```bash
330+
# project.py
331+
332+
registry.write_local_mtz(
333+
mtz_path: str = "./local.mtz", # path to the local .mtz file
334+
working_dir: str = ".",
335+
command: str = "python3", # for a venv you might want to use `./venv/bin/python3`
336+
params: str = "project.py",
337+
debug: bool = True
338+
)
339+
```
340+
308341
## Legacy Transforms
309342

310343
[Documentation](https://docs.maltego.com/support/solutions/articles/15000018299-porting-old-trx-transforms-to-the-latest-version)

maltego_trx/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
VERSION = "1.5.2"
1+
VERSION = "1.6.0"

maltego_trx/decorator_registry.py

+186-52
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,41 @@
1+
import logging
12
import os
3+
import zipfile
4+
from collections import defaultdict
25
from dataclasses import dataclass, field
36
from itertools import chain
4-
from typing import List, Optional, Dict, Iterable
7+
from typing import List, Optional, Dict, Iterable, Tuple
58

6-
from maltego_trx.utils import filter_unique, pascal_case_to_title, escape_csv_fields, export_as_csv, serialize_bool, \
7-
name_to_path
9+
from maltego_trx.mtz import (
10+
create_local_server_xml,
11+
create_settings_xml,
12+
create_transform_xml,
13+
create_transform_set_xml,
14+
)
15+
from maltego_trx.utils import (
16+
filter_unique,
17+
pascal_case_to_title,
18+
escape_csv_fields,
19+
export_as_csv,
20+
serialize_bool,
21+
name_to_path,
22+
serialize_xml,
23+
)
824

9-
TRANSFORMS_CSV_HEADER = "Owner,Author,Disclaimer,Description,Version," \
10-
"Name,UIName,URL,entityName," \
11-
"oAuthSettingId,transformSettingIDs,seedIDs"
25+
TRANSFORMS_CSV_HEADER = (
26+
"Owner,Author,Disclaimer,Description,Version,"
27+
"Name,UIName,URL,entityName,"
28+
"oAuthSettingId,transformSettingIDs,seedIDs"
29+
)
1230
SETTINGS_CSV_HEADER = "Name,Type,Display,DefaultValue,Optional,Popup"
1331

1432

33+
@dataclass(frozen=True)
34+
class TransformSet:
35+
name: str
36+
description: str
37+
38+
1539
@dataclass()
1640
class TransformMeta:
1741
class_name: str
@@ -20,9 +44,10 @@ class TransformMeta:
2044
description: str
2145
output_entities: List[str]
2246
disclaimer: str
47+
transform_set: TransformSet
2348

2449

25-
@dataclass()
50+
@dataclass(frozen=True)
2651
class TransformSetting:
2752
name: str
2853
display_name: str
@@ -49,87 +74,196 @@ class TransformRegistry:
4974
host_url: str
5075
seed_ids: List[str]
5176

52-
version: str = '0.1'
77+
version: str = "0.1"
5378
display_name_suffix: str = ""
5479

5580
global_settings: List[TransformSetting] = field(default_factory=list)
5681
oauth_settings_id: Optional[str] = ""
5782

5883
transform_metas: Dict[str, TransformMeta] = field(init=False, default_factory=dict)
59-
transform_settings: Dict[str, List[TransformSetting]] = field(init=False, default_factory=dict)
84+
transform_settings: Dict[str, List[TransformSetting]] = field(
85+
init=False, default_factory=dict
86+
)
87+
transform_sets: Dict[TransformSet, List[str]] = field(
88+
init=False, default_factory=lambda: defaultdict(list)
89+
)
6090

61-
def register_transform(self, display_name: str, input_entity: str, description: str,
62-
settings: List[TransformSetting] = None, output_entities: List[str] = None,
63-
disclaimer: str = ""):
64-
""" This method can be used as a decorator on transform classes. The data will be used to fill out csv config
65-
files to be imported into a TDS.
91+
def register_transform(
92+
self,
93+
display_name: str,
94+
input_entity: str,
95+
description: str,
96+
settings: List[TransformSetting] = None,
97+
output_entities: List[str] = None,
98+
disclaimer: str = "",
99+
transform_set: TransformSet = None,
100+
):
101+
"""This method can be used as a decorator on transform classes. The data will be used to fill out csv config
102+
files to be imported into a TDS.
66103
"""
67104

68105
def decorated(transform_callable: object):
69106
cleaned_transform_name = name_to_path(transform_callable.__name__)
70107
display = display_name or pascal_case_to_title(transform_callable.__name__)
71108

72-
meta = TransformMeta(cleaned_transform_name,
73-
display, input_entity,
74-
description,
75-
output_entities or [],
76-
disclaimer)
109+
meta = TransformMeta(
110+
cleaned_transform_name,
111+
display,
112+
input_entity,
113+
description,
114+
output_entities or [],
115+
disclaimer,
116+
transform_set=transform_set,
117+
)
77118
self.transform_metas[cleaned_transform_name] = meta
78119

79120
if settings:
80121
self.transform_settings[cleaned_transform_name] = settings
81122

123+
if transform_set:
124+
self.transform_sets[transform_set].append(cleaned_transform_name)
125+
82126
return transform_callable
83127

84128
return decorated
85129

86-
def write_transforms_config(self, config_path: str = "./transforms.csv", csv_line_limit: int = 100):
87-
"""Exports the collected transform metadata as a csv-file to config_path"""
130+
def _create_transforms_config(self) -> Iterable[str]:
88131
global_settings_full_names = [gs.id for gs in self.global_settings]
89132

90-
csv_lines = []
91133
for transform_name, transform_meta in self.transform_metas.items():
92-
meta_settings = [setting.id for setting in
93-
self.transform_settings.get(transform_name, [])]
134+
meta_settings = [
135+
setting.id
136+
for setting in self.transform_settings.get(transform_name, [])
137+
]
94138

95139
transform_row = [
96-
self.owner,
97-
self.author,
98-
transform_meta.disclaimer,
99-
transform_meta.description,
100-
self.version,
101-
transform_name,
102-
transform_meta.display_name + self.display_name_suffix,
103-
os.path.join(self.host_url, "run", transform_name),
104-
transform_meta.input_entity,
105-
";".join(self.oauth_settings_id),
106-
# combine global and transform scoped settings
107-
";".join(chain(meta_settings, global_settings_full_names)),
108-
";".join(self.seed_ids)
140+
self.owner,
141+
self.author,
142+
transform_meta.disclaimer,
143+
transform_meta.description,
144+
self.version,
145+
transform_name,
146+
transform_meta.display_name + self.display_name_suffix,
147+
os.path.join(self.host_url, "run", transform_name),
148+
transform_meta.input_entity,
149+
";".join(self.oauth_settings_id),
150+
# combine global and transform scoped settings
151+
";".join(chain(meta_settings, global_settings_full_names)),
152+
";".join(self.seed_ids),
109153
]
110154

111155
escaped_fields = escape_csv_fields(*transform_row)
112-
csv_lines.append(",".join(escaped_fields))
156+
yield ",".join(escaped_fields)
157+
158+
def write_transforms_config(
159+
self, config_path: str = "./transforms.csv", csv_line_limit: int = 100
160+
):
161+
"""Exports the collected transform metadata as a csv-file to config_path"""
113162

114-
export_as_csv(TRANSFORMS_CSV_HEADER, csv_lines, config_path, csv_line_limit)
163+
csv_lines = self._create_transforms_config()
115164

116-
def write_settings_config(self, config_path: str = "./settings.csv", csv_line_limit: int = 100):
117-
"""Exports the collected settings metadata as a csv-file to config_path"""
118-
chained_settings = chain(self.global_settings, *list(self.transform_settings.values()))
119-
unique_settings: Iterable[TransformSetting] = filter_unique(lambda s: s.name, chained_settings)
165+
export_as_csv(
166+
TRANSFORMS_CSV_HEADER, tuple(csv_lines), config_path, csv_line_limit
167+
)
168+
169+
def _create_settings_config(self) -> Iterable[str]:
170+
chained_settings = chain(
171+
self.global_settings, *list(self.transform_settings.values())
172+
)
173+
unique_settings: Iterable[TransformSetting] = filter_unique(
174+
lambda s: s.name, chained_settings
175+
)
120176

121-
csv_lines = []
122177
for setting in unique_settings:
123178
setting_row = [
124-
setting.id,
125-
setting.setting_type,
126-
setting.display_name,
127-
setting.default_value or "",
128-
serialize_bool(setting.optional, 'True', 'False'),
129-
serialize_bool(setting.popup, 'Yes', 'No')
179+
setting.id,
180+
setting.setting_type,
181+
setting.display_name,
182+
setting.default_value or "",
183+
serialize_bool(setting.optional, "True", "False"),
184+
serialize_bool(setting.popup, "Yes", "No"),
130185
]
131186

132187
escaped_fields = escape_csv_fields(*setting_row)
133-
csv_lines.append(",".join(escaped_fields))
188+
yield ",".join(escaped_fields)
189+
190+
def write_settings_config(
191+
self, config_path: str = "./settings.csv", csv_line_limit: int = 100
192+
):
193+
"""Exports the collected settings metadata as a csv-file to config_path"""
194+
195+
csv_lines = self._create_settings_config()
196+
197+
export_as_csv(
198+
SETTINGS_CSV_HEADER, tuple(csv_lines), config_path, csv_line_limit
199+
)
200+
201+
def _create_local_mtz(
202+
self,
203+
working_dir: str = ".",
204+
command: str = "python3",
205+
params: str = "project.py",
206+
debug: bool = True,
207+
) -> Iterable[Tuple[str, str]]:
208+
working_dir = os.path.abspath(working_dir)
209+
if self.global_settings:
210+
logging.warning(
211+
f"Settings are not supported with local transforms. "
212+
f"Global settings are: {', '.join(map(lambda s: s.name, self.global_settings))}"
213+
)
214+
215+
"""Creates an .mtz for bulk importing local transforms"""
216+
server_xml = create_local_server_xml(self.transform_metas.keys())
217+
218+
server_xml_str = serialize_xml(server_xml)
219+
yield "Servers/Local.tas", server_xml_str
220+
221+
for name, meta in self.transform_metas.items():
222+
settings_xml = create_settings_xml(
223+
working_dir, command, f"{params} local {name}", debug
224+
)
225+
settings_xml_str = serialize_xml(settings_xml)
226+
227+
tx_settings = self.transform_settings.get(name)
228+
if tx_settings:
229+
logging.warning(
230+
"Settings are not supported with local transforms. "
231+
f"Transform '{meta.display_name}' has: {', '.join(map(lambda s: s.name, tx_settings))}"
232+
)
233+
234+
xml = create_transform_xml(
235+
name,
236+
meta.display_name,
237+
meta.description,
238+
meta.input_entity,
239+
self.author,
240+
)
241+
242+
xml_str = serialize_xml(xml)
243+
244+
yield f"TransformRepositories/Local/{name}.transform", xml_str
245+
yield f"TransformRepositories/Local/{name}.transformsettings", settings_xml_str
246+
247+
for transform_set, transforms in self.transform_sets.items():
248+
set_xml = create_transform_set_xml(
249+
transform_set.name, transform_set.description, transforms
250+
)
251+
252+
set_xml_str = serialize_xml(set_xml)
253+
254+
yield f"TransformSets/{transform_set.name}.set", set_xml_str
255+
256+
def write_local_mtz(
257+
self,
258+
mtz_path: str = "./local.mtz",
259+
working_dir: str = ".",
260+
command: str = "python3",
261+
params: str = "project.py",
262+
debug: bool = True,
263+
):
134264

135-
export_as_csv(SETTINGS_CSV_HEADER, csv_lines, config_path, csv_line_limit)
265+
with zipfile.ZipFile(mtz_path, "w") as mtz:
266+
for path, content in self._create_local_mtz(
267+
working_dir, command, params, debug
268+
):
269+
mtz.writestr(path, content)

0 commit comments

Comments
 (0)