mal-toolbox 1.0.6__tar.gz → 1.0.7__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {mal_toolbox-1.0.6/mal_toolbox.egg-info → mal_toolbox-1.0.7}/PKG-INFO +1 -1
- {mal_toolbox-1.0.6 → mal_toolbox-1.0.7/mal_toolbox.egg-info}/PKG-INFO +1 -1
- {mal_toolbox-1.0.6 → mal_toolbox-1.0.7}/mal_toolbox.egg-info/SOURCES.txt +2 -0
- {mal_toolbox-1.0.6 → mal_toolbox-1.0.7}/maltoolbox/__init__.py +2 -2
- {mal_toolbox-1.0.6 → mal_toolbox-1.0.7}/maltoolbox/__main__.py +6 -4
- {mal_toolbox-1.0.6 → mal_toolbox-1.0.7}/maltoolbox/visualization/__init__.py +3 -1
- mal_toolbox-1.0.7/maltoolbox/visualization/draw_io_utils.py +317 -0
- mal_toolbox-1.0.7/maltoolbox/visualization/utils.py +41 -0
- {mal_toolbox-1.0.6 → mal_toolbox-1.0.7}/pyproject.toml +1 -1
- {mal_toolbox-1.0.6 → mal_toolbox-1.0.7}/AUTHORS +0 -0
- {mal_toolbox-1.0.6 → mal_toolbox-1.0.7}/LICENSE +0 -0
- {mal_toolbox-1.0.6 → mal_toolbox-1.0.7}/README.md +0 -0
- {mal_toolbox-1.0.6 → mal_toolbox-1.0.7}/mal_toolbox.egg-info/dependency_links.txt +0 -0
- {mal_toolbox-1.0.6 → mal_toolbox-1.0.7}/mal_toolbox.egg-info/entry_points.txt +0 -0
- {mal_toolbox-1.0.6 → mal_toolbox-1.0.7}/mal_toolbox.egg-info/requires.txt +0 -0
- {mal_toolbox-1.0.6 → mal_toolbox-1.0.7}/mal_toolbox.egg-info/top_level.txt +0 -0
- {mal_toolbox-1.0.6 → mal_toolbox-1.0.7}/maltoolbox/attackgraph/__init__.py +0 -0
- {mal_toolbox-1.0.6 → mal_toolbox-1.0.7}/maltoolbox/attackgraph/analyzers/__init__.py +0 -0
- {mal_toolbox-1.0.6 → mal_toolbox-1.0.7}/maltoolbox/attackgraph/attackgraph.py +0 -0
- {mal_toolbox-1.0.6 → mal_toolbox-1.0.7}/maltoolbox/attackgraph/node.py +0 -0
- {mal_toolbox-1.0.6 → mal_toolbox-1.0.7}/maltoolbox/exceptions.py +0 -0
- {mal_toolbox-1.0.6 → mal_toolbox-1.0.7}/maltoolbox/file_utils.py +0 -0
- {mal_toolbox-1.0.6 → mal_toolbox-1.0.7}/maltoolbox/language/__init__.py +0 -0
- {mal_toolbox-1.0.6 → mal_toolbox-1.0.7}/maltoolbox/language/compiler/__init__.py +0 -0
- {mal_toolbox-1.0.6 → mal_toolbox-1.0.7}/maltoolbox/language/compiler/mal_lexer.py +0 -0
- {mal_toolbox-1.0.6 → mal_toolbox-1.0.7}/maltoolbox/language/compiler/mal_parser.py +0 -0
- {mal_toolbox-1.0.6 → mal_toolbox-1.0.7}/maltoolbox/language/languagegraph.py +0 -0
- {mal_toolbox-1.0.6 → mal_toolbox-1.0.7}/maltoolbox/model.py +0 -0
- {mal_toolbox-1.0.6 → mal_toolbox-1.0.7}/maltoolbox/patternfinder/__init__.py +0 -0
- {mal_toolbox-1.0.6 → mal_toolbox-1.0.7}/maltoolbox/patternfinder/attackgraph_patterns.py +0 -0
- {mal_toolbox-1.0.6 → mal_toolbox-1.0.7}/maltoolbox/py.typed +0 -0
- {mal_toolbox-1.0.6 → mal_toolbox-1.0.7}/maltoolbox/translators/__init__.py +0 -0
- {mal_toolbox-1.0.6 → mal_toolbox-1.0.7}/maltoolbox/translators/securicad.py +0 -0
- {mal_toolbox-1.0.6 → mal_toolbox-1.0.7}/maltoolbox/translators/updater.py +0 -0
- {mal_toolbox-1.0.6 → mal_toolbox-1.0.7}/maltoolbox/visualization/graphviz_utils.py +0 -0
- {mal_toolbox-1.0.6 → mal_toolbox-1.0.7}/maltoolbox/visualization/neo4j_utils.py +0 -0
- {mal_toolbox-1.0.6 → mal_toolbox-1.0.7}/setup.cfg +0 -0
- {mal_toolbox-1.0.6 → mal_toolbox-1.0.7}/tests/test_model.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mal-toolbox
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.7
|
|
4
4
|
Summary: A collection of tools used to create MAL models and attack graphs.
|
|
5
5
|
Author-email: Andrei Buhaiu <buhaiu@kth.se>, Joakim Loxdal <loxdal@kth.se>, Nikolaos Kakouros <nkak@kth.se>, Jakob Nyberg <jaknyb@kth.se>, Giuseppe Nebbione <nebbione@kth.se>, Sandor Berglund <sandor@kth.se>
|
|
6
6
|
License: Apache Software License
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mal-toolbox
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.7
|
|
4
4
|
Summary: A collection of tools used to create MAL models and attack graphs.
|
|
5
5
|
Author-email: Andrei Buhaiu <buhaiu@kth.se>, Joakim Loxdal <loxdal@kth.se>, Nikolaos Kakouros <nkak@kth.se>, Jakob Nyberg <jaknyb@kth.se>, Giuseppe Nebbione <nebbione@kth.se>, Sandor Berglund <sandor@kth.se>
|
|
6
6
|
License: Apache Software License
|
|
@@ -29,6 +29,8 @@ maltoolbox/translators/__init__.py
|
|
|
29
29
|
maltoolbox/translators/securicad.py
|
|
30
30
|
maltoolbox/translators/updater.py
|
|
31
31
|
maltoolbox/visualization/__init__.py
|
|
32
|
+
maltoolbox/visualization/draw_io_utils.py
|
|
32
33
|
maltoolbox/visualization/graphviz_utils.py
|
|
33
34
|
maltoolbox/visualization/neo4j_utils.py
|
|
35
|
+
maltoolbox/visualization/utils.py
|
|
34
36
|
tests/test_model.py
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# -*- encoding: utf-8 -*-
|
|
2
|
-
# MAL Toolbox v1.0.
|
|
2
|
+
# MAL Toolbox v1.0.7
|
|
3
3
|
# Copyright 2025, Andrei Buhaiu.
|
|
4
4
|
#
|
|
5
5
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
@@ -21,7 +21,7 @@ MAL-Toolbox Framework
|
|
|
21
21
|
"""
|
|
22
22
|
|
|
23
23
|
__title__ = "maltoolbox"
|
|
24
|
-
__version__ = "1.0.
|
|
24
|
+
__version__ = "1.0.7"
|
|
25
25
|
__authors__ = [
|
|
26
26
|
"Andrei Buhaiu",
|
|
27
27
|
"Giuseppe Nebbione",
|
|
@@ -5,7 +5,7 @@ Usage:
|
|
|
5
5
|
maltoolbox compile <lang_file> <output_file>
|
|
6
6
|
maltoolbox generate-attack-graph [--graphviz] [--neo4j] <model_file> <lang_file>
|
|
7
7
|
maltoolbox upgrade-model <model_file> <lang_file> <output_file>
|
|
8
|
-
maltoolbox visualize-model [--neo4j] [--graphviz] <model_file> <lang_file>
|
|
8
|
+
maltoolbox visualize-model [--neo4j] [--graphviz] [-drawio] <model_file> <lang_file>
|
|
9
9
|
|
|
10
10
|
Arguments:
|
|
11
11
|
<model_file> Path to JSON instance model file.
|
|
@@ -16,6 +16,7 @@ Options:
|
|
|
16
16
|
-h --help Show this screen.
|
|
17
17
|
-g --graphviz Visualize with graphviz
|
|
18
18
|
-n --neo4j Send to neo4j
|
|
19
|
+
-d --drawio Export draw.io file
|
|
19
20
|
|
|
20
21
|
Notes:
|
|
21
22
|
- <lang_file> can be either a .mar file (generated by the older MAL
|
|
@@ -35,7 +36,8 @@ from .visualization import (
|
|
|
35
36
|
render_model,
|
|
36
37
|
render_attack_graph,
|
|
37
38
|
ingest_model_neo4j,
|
|
38
|
-
ingest_attack_graph_neo4j
|
|
39
|
+
ingest_attack_graph_neo4j,
|
|
40
|
+
create_drawio_file_with_images
|
|
39
41
|
)
|
|
40
42
|
from .model import Model
|
|
41
43
|
|
|
@@ -100,10 +102,10 @@ def main():
|
|
|
100
102
|
model = Model.load_from_file(args['<model_file>'], lang_graph)
|
|
101
103
|
if args['--graphviz']:
|
|
102
104
|
render_model(model)
|
|
103
|
-
else:
|
|
104
|
-
print("Use flag --graphviz to generate a pdf")
|
|
105
105
|
if args['--neo4j']:
|
|
106
106
|
ingest_model_neo4j(model, neo4j_configs)
|
|
107
|
+
if args['--drawio']:
|
|
108
|
+
create_drawio_file_with_images(model)
|
|
107
109
|
|
|
108
110
|
if __name__ == "__main__":
|
|
109
111
|
main()
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
from .graphviz_utils import render_attack_graph, render_model
|
|
2
2
|
from .neo4j_utils import ingest_attack_graph_neo4j, ingest_model_neo4j
|
|
3
|
+
from .draw_io_utils import create_drawio_file_with_images
|
|
3
4
|
|
|
4
5
|
__all__ = [
|
|
5
6
|
'render_attack_graph',
|
|
6
7
|
'render_model',
|
|
7
8
|
'ingest_attack_graph_neo4j',
|
|
8
|
-
'ingest_model_neo4j'
|
|
9
|
+
'ingest_model_neo4j',
|
|
10
|
+
'create_drawio_file_with_images'
|
|
9
11
|
]
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
"""DrawIO exporter made by Sandor"""
|
|
2
|
+
import xml.etree.ElementTree as ET
|
|
3
|
+
from xml.dom import minidom
|
|
4
|
+
import math
|
|
5
|
+
|
|
6
|
+
from maltoolbox.model import Model
|
|
7
|
+
|
|
8
|
+
from .utils import position_assets
|
|
9
|
+
|
|
10
|
+
type2iconURL = {
|
|
11
|
+
"Hardware": "https://uxwing.com/wp-content/themes/uxwing/download/domain-hosting/server-rack-outline-icon.png",
|
|
12
|
+
"SoftwareProduct": "https://uxwing.com/wp-content/themes/uxwing/download/logistics-shipping-delivery/packing-icon.png",
|
|
13
|
+
"Application": "https://uxwing.com/wp-content/themes/uxwing/download/web-app-development/coding-icon.png",
|
|
14
|
+
"IDPS": "https://uxwing.com/wp-content/themes/uxwing/download/web-app-development/web-page-source-code-icon.png",
|
|
15
|
+
"PhysicalZone": "https://uxwing.com/wp-content/themes/uxwing/download/location-travel-map/address-location-icon.png",
|
|
16
|
+
"Information": "https://uxwing.com/wp-content/themes/uxwing/download/web-app-development/more-info-icon.png",
|
|
17
|
+
"Data": "https://uxwing.com/wp-content/themes/uxwing/download/web-app-development/database-line-icon.png",
|
|
18
|
+
"IAMObject": "https://uxwing.com/wp-content/themes/uxwing/download/communication-chat-call/name-id-icon.png",
|
|
19
|
+
"Identity": "https://uxwing.com/wp-content/themes/uxwing/download/communication-chat-call/name-id-icon.png",
|
|
20
|
+
"Privileges": "https://uxwing.com/wp-content/themes/uxwing/download/banking-finance/access-hand-key-icon.png",
|
|
21
|
+
"Group": "https://uxwing.com/wp-content/themes/uxwing/download/business-professional-services/team-icon.png",
|
|
22
|
+
"Credentials": "https://uxwing.com/wp-content/themes/uxwing/download/household-and-furniture/key-line-icon.png",
|
|
23
|
+
"User": "https://uxwing.com/wp-content/themes/uxwing/download/peoples-avatars/unisex-male-and-female-icon.png",
|
|
24
|
+
"Network": "https://uxwing.com/wp-content/themes/uxwing/download/internet-network-technology/big-data-icon.png",
|
|
25
|
+
"RoutingFirewall": "https://uxwing.com/wp-content/themes/uxwing/download/internet-network-technology/encryption-icon.png",
|
|
26
|
+
"ConnectionRule": "https://uxwing.com/wp-content/themes/uxwing/download/internet-network-technology/pc-network-icon.png",
|
|
27
|
+
"Vulnerability": "https://uxwing.com/wp-content/themes/uxwing/download/web-app-development/cyber-security-icon.png",
|
|
28
|
+
"SoftwareVulnerability": "https://uxwing.com/wp-content/themes/uxwing/download/web-app-development/cyber-security-icon.png",
|
|
29
|
+
"HardwareVulnerability": "https://uxwing.com/wp-content/themes/uxwing/download/crime-security-military-law/shield-sedo-line-icon.png",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
def create_drawio_file_with_images(
|
|
33
|
+
model: Model,
|
|
34
|
+
show_edge_labels=True,
|
|
35
|
+
line_thickness=2,
|
|
36
|
+
coordinate_scale=0.75,
|
|
37
|
+
output_filename=None
|
|
38
|
+
):
|
|
39
|
+
"""
|
|
40
|
+
Create a draw.io file with all model assets as boxes using their actual positions and images
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
model: The model containing assets and associations
|
|
44
|
+
output_filename: Name of the output draw.io file
|
|
45
|
+
show_edge_labels: If True, show association type as text on edges. If False, edges will have no labels.
|
|
46
|
+
line_thickness: Thickness of the edges in pixels (default: 2)
|
|
47
|
+
coordinate_scale: Scale factor for model coordinates (default: 1.0, use 0.5 for half size, 2.0 for double size)
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
if not all(a.extras.get('position') for a in model.assets.values()):
|
|
51
|
+
# Give assets positions if not already set
|
|
52
|
+
position_assets(model)
|
|
53
|
+
|
|
54
|
+
output_filename = output_filename or (
|
|
55
|
+
(model.name or "model_assets_with_images") + ".drawio"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# Use the type2iconURL mapping for image URLs
|
|
59
|
+
type_images = type2iconURL
|
|
60
|
+
|
|
61
|
+
# Create root element
|
|
62
|
+
root = ET.Element("mxfile")
|
|
63
|
+
root.set("host", "app.diagrams.net")
|
|
64
|
+
root.set("modified", "2024-01-01T00:00:00.000Z")
|
|
65
|
+
root.set("agent", "5.0")
|
|
66
|
+
root.set("version", "24.7.17")
|
|
67
|
+
root.set("et", "https://www.diagrams.net/")
|
|
68
|
+
root.set("type", "device")
|
|
69
|
+
|
|
70
|
+
# Create diagram element
|
|
71
|
+
diagram = ET.SubElement(root, "diagram")
|
|
72
|
+
diagram.set("id", "model-assets")
|
|
73
|
+
diagram.set("name", "Model Assets")
|
|
74
|
+
|
|
75
|
+
# Create mxGraphModel
|
|
76
|
+
mxgraph = ET.SubElement(diagram, "mxGraphModel")
|
|
77
|
+
mxgraph.set("dx", "1422")
|
|
78
|
+
mxgraph.set("dy", "754")
|
|
79
|
+
mxgraph.set("grid", "1")
|
|
80
|
+
mxgraph.set("gridSize", "10")
|
|
81
|
+
mxgraph.set("guides", "1")
|
|
82
|
+
mxgraph.set("tooltips", "1")
|
|
83
|
+
mxgraph.set("connect", "1")
|
|
84
|
+
mxgraph.set("arrows", "1")
|
|
85
|
+
mxgraph.set("fold", "1")
|
|
86
|
+
mxgraph.set("page", "1")
|
|
87
|
+
mxgraph.set("pageScale", "1")
|
|
88
|
+
mxgraph.set("pageWidth", "2000")
|
|
89
|
+
mxgraph.set("pageHeight", "1500")
|
|
90
|
+
mxgraph.set("math", "0")
|
|
91
|
+
mxgraph.set("shadow", "0")
|
|
92
|
+
|
|
93
|
+
# Create root cell
|
|
94
|
+
root_cell = ET.SubElement(mxgraph, "root")
|
|
95
|
+
|
|
96
|
+
# Create default parent
|
|
97
|
+
default_parent = ET.SubElement(root_cell, "mxCell")
|
|
98
|
+
default_parent.set("id", "0")
|
|
99
|
+
|
|
100
|
+
# Create parent for edges (back layer)
|
|
101
|
+
edges_parent = ET.SubElement(root_cell, "mxCell")
|
|
102
|
+
edges_parent.set("id", "1")
|
|
103
|
+
edges_parent.set("parent", "0")
|
|
104
|
+
|
|
105
|
+
# Create default parent for model (front layer)
|
|
106
|
+
model_parent = ET.SubElement(root_cell, "mxCell")
|
|
107
|
+
model_parent.set("id", "2")
|
|
108
|
+
model_parent.set("parent", "0")
|
|
109
|
+
|
|
110
|
+
# Get assets and use their actual positions
|
|
111
|
+
assets = list(model.assets.values())
|
|
112
|
+
|
|
113
|
+
# Box dimensions
|
|
114
|
+
box_width = 150
|
|
115
|
+
box_height = 80
|
|
116
|
+
|
|
117
|
+
# Find the bounds of all assets to center the diagram
|
|
118
|
+
min_x = min_y = float("inf")
|
|
119
|
+
max_x = max_y = float("-inf")
|
|
120
|
+
|
|
121
|
+
for asset in assets:
|
|
122
|
+
if asset.extras:
|
|
123
|
+
x = asset.extras.get("position", {}).get("x", 0)
|
|
124
|
+
y = asset.extras.get("position", {}).get("y", 0)
|
|
125
|
+
min_x = min(min_x, x)
|
|
126
|
+
min_y = min(min_y, y)
|
|
127
|
+
max_x = max(max_x, x)
|
|
128
|
+
max_y = max(max_y, y)
|
|
129
|
+
|
|
130
|
+
# If no positions found, use default bounds
|
|
131
|
+
if min_x == float("inf"):
|
|
132
|
+
min_x = min_y = 0
|
|
133
|
+
max_x = max_y = 1000
|
|
134
|
+
|
|
135
|
+
# Add some padding
|
|
136
|
+
padding = 100
|
|
137
|
+
min_x -= padding
|
|
138
|
+
min_y -= padding
|
|
139
|
+
max_x += padding
|
|
140
|
+
max_y += padding
|
|
141
|
+
|
|
142
|
+
# Create boxes for each asset using their actual positions
|
|
143
|
+
for asset in assets:
|
|
144
|
+
|
|
145
|
+
# Get position from extras, with fallback to grid layout
|
|
146
|
+
if hasattr(asset, "extras") and asset.extras and "position" in asset.extras:
|
|
147
|
+
x = round(asset.extras["position"].get("x", 0) * coordinate_scale)
|
|
148
|
+
y = round(asset.extras["position"].get("y", 0) * coordinate_scale)
|
|
149
|
+
else:
|
|
150
|
+
# Fallback to grid layout if no position data
|
|
151
|
+
i = list(model.assets.keys()).index(asset.id)
|
|
152
|
+
cols = math.ceil(math.sqrt(len(assets)))
|
|
153
|
+
row = i // cols
|
|
154
|
+
col = i % cols
|
|
155
|
+
x = round((50 + col * 200) * coordinate_scale)
|
|
156
|
+
y = round((50 + row * 120) * coordinate_scale)
|
|
157
|
+
|
|
158
|
+
# Get image URL for asset type
|
|
159
|
+
image_url = type_images.get(
|
|
160
|
+
asset.type,
|
|
161
|
+
"https://uxwing.com/wp-content/themes/uxwing/download/communication-chat-call/question-mark-icon.png",
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# Define colors for each asset type (matching the design)
|
|
165
|
+
type_colors = {
|
|
166
|
+
"Hardware": "#4CAF50", # Green
|
|
167
|
+
"Application": "#2196F3", # Blue
|
|
168
|
+
"Network": "#9C27B0", # Purple
|
|
169
|
+
"ConnectionRule": "#FF9800", # Orange
|
|
170
|
+
"Identity": "#607D8B", # Blue Grey
|
|
171
|
+
"Credentials": "#4CAF50", # Green
|
|
172
|
+
"SoftwareVulnerability": "#F44336", # Red
|
|
173
|
+
"Data": "#795548", # Brown
|
|
174
|
+
"User": "#00BCD4", # Cyan
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
# Get color for this asset type
|
|
178
|
+
top_color = type_colors.get(asset.type, "#4CAF50")
|
|
179
|
+
|
|
180
|
+
# Create the HTML content for the two-segment box
|
|
181
|
+
html_content = f"""
|
|
182
|
+
<div style="width: {box_width}px; height: {box_height}px; position: relative; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
|
|
183
|
+
<!-- Top segment with icon and type -->
|
|
184
|
+
<div style="background: linear-gradient(to bottom, {top_color}, {top_color}dd); height: 40%; display: flex; align-items: center; padding: 0 8px; position: relative;">
|
|
185
|
+
<img src="{image_url}" width="20" height="20" style="margin-right: 8px;"/>
|
|
186
|
+
<span style="color: white; font-weight: bold; font-size: 11px; flex: 1;">{asset.type}</span>
|
|
187
|
+
<div style="width: 8px; height: 8px; background: {top_color}; position: absolute; top: 4px; right: 4px; border-radius: 2px;"></div>
|
|
188
|
+
</div>
|
|
189
|
+
<!-- Bottom segment with asset name -->
|
|
190
|
+
<div style="background: #424242; height: 60%; display: flex; align-items: center; justify-content: center; padding: 0 8px;">
|
|
191
|
+
<span style="color: white; font-size: 10px; text-align: center; line-height: 1.2;">{asset.name}</span>
|
|
192
|
+
</div>
|
|
193
|
+
<!-- Connecting line -->
|
|
194
|
+
<div style="position: absolute; bottom: 40%; left: 0; width: 0; height: 0; border-left: 8px solid transparent; border-right: 8px solid transparent; border-top: 8px solid {top_color};"></div>
|
|
195
|
+
</div>
|
|
196
|
+
"""
|
|
197
|
+
|
|
198
|
+
# Create mxCell for the asset box
|
|
199
|
+
cell = ET.SubElement(root_cell, "mxCell")
|
|
200
|
+
cell.set("id", f"asset_{asset.id}")
|
|
201
|
+
cell.set("value", html_content)
|
|
202
|
+
cell.set(
|
|
203
|
+
"style",
|
|
204
|
+
"rounded=0;whiteSpace=wrap;html=1;overflow=fill;"
|
|
205
|
+
"align=center;verticalAlign=middle;spacing=0;"
|
|
206
|
+
"fillColor=none;strokeColor=none;fontSize=10;fontStyle=1;",
|
|
207
|
+
)
|
|
208
|
+
cell.set("vertex", "1")
|
|
209
|
+
cell.set("parent", "2") # Put assets in front layer
|
|
210
|
+
|
|
211
|
+
# Create geometry element
|
|
212
|
+
geometry = ET.SubElement(cell, "mxGeometry")
|
|
213
|
+
geometry.set("x", str(x))
|
|
214
|
+
geometry.set("y", str(y))
|
|
215
|
+
geometry.set("width", str(box_width))
|
|
216
|
+
geometry.set("height", str(box_height))
|
|
217
|
+
geometry.set("as", "geometry")
|
|
218
|
+
|
|
219
|
+
# Create edges for associations (avoiding duplicates)
|
|
220
|
+
edge_id = 1000 # Start edge IDs from 1000 to avoid conflicts with asset IDs
|
|
221
|
+
processed_edges = set() # Track processed edges to avoid duplicates
|
|
222
|
+
|
|
223
|
+
for asset in assets:
|
|
224
|
+
if hasattr(asset, "associated_assets") and asset.associated_assets:
|
|
225
|
+
for asset_assoc_field, associated_assets in asset.associated_assets.items():
|
|
226
|
+
association_type = asset.lg_asset.associations[asset_assoc_field].name
|
|
227
|
+
for assoc_asset in associated_assets:
|
|
228
|
+
# Create a unique edge identifier (sorted to handle bidirectional associations)
|
|
229
|
+
edge_key = tuple(sorted([asset.id, assoc_asset.id]))
|
|
230
|
+
|
|
231
|
+
# Skip if we've already processed this edge
|
|
232
|
+
if edge_key in processed_edges:
|
|
233
|
+
continue
|
|
234
|
+
|
|
235
|
+
# Find the target asset
|
|
236
|
+
target_asset = None
|
|
237
|
+
for a in assets:
|
|
238
|
+
if a.id == assoc_asset.id:
|
|
239
|
+
target_asset = a
|
|
240
|
+
break
|
|
241
|
+
|
|
242
|
+
if target_asset:
|
|
243
|
+
# Mark this edge as processed
|
|
244
|
+
processed_edges.add(edge_key)
|
|
245
|
+
# Get positions for both assets
|
|
246
|
+
source_x = asset.extras.get("position", {}).get("x", 0) + box_width / 2
|
|
247
|
+
source_y = asset.extras.get("position", {}).get("y", 0) + box_height / 2
|
|
248
|
+
target_x = (
|
|
249
|
+
target_asset.extras.get("position", {}).get("x", 0) + box_width / 2
|
|
250
|
+
)
|
|
251
|
+
target_y = (
|
|
252
|
+
target_asset.extras.get("position", {}).get("y", 0) + box_height / 2
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
# Create edge cell
|
|
256
|
+
edge_cell = ET.SubElement(root_cell, "mxCell")
|
|
257
|
+
edge_cell.set("id", f"edge_{edge_id}")
|
|
258
|
+
edge_cell.set(
|
|
259
|
+
"value", association_type if show_edge_labels else ""
|
|
260
|
+
)
|
|
261
|
+
edge_cell.set(
|
|
262
|
+
"style",
|
|
263
|
+
f"edgeStyle=straightEdgeStyle;rounded=0;html=1;fontSize=10;fontStyle=1;endArrow=none;startArrow=none;strokeWidth={line_thickness};",
|
|
264
|
+
)
|
|
265
|
+
edge_cell.set("edge", "1")
|
|
266
|
+
edge_cell.set("parent", "1") # Put edges in back layer
|
|
267
|
+
edge_cell.set("source", f"asset_{asset.id}")
|
|
268
|
+
edge_cell.set("target", f"asset_{assoc_asset.id}")
|
|
269
|
+
|
|
270
|
+
# Create geometry for edge
|
|
271
|
+
edge_geometry = ET.SubElement(edge_cell, "mxGeometry")
|
|
272
|
+
edge_geometry.set("relative", "1")
|
|
273
|
+
edge_geometry.set("as", "geometry")
|
|
274
|
+
|
|
275
|
+
# Create array of points for the edge
|
|
276
|
+
array = ET.SubElement(edge_geometry, "Array")
|
|
277
|
+
array.set("as", "points")
|
|
278
|
+
|
|
279
|
+
# Add source point
|
|
280
|
+
point1 = ET.SubElement(array, "mxPoint")
|
|
281
|
+
point1.set("x", str(source_x))
|
|
282
|
+
point1.set("y", str(source_y))
|
|
283
|
+
point1.set("as", "sourcePoint")
|
|
284
|
+
|
|
285
|
+
# Add target point
|
|
286
|
+
point2 = ET.SubElement(array, "mxPoint")
|
|
287
|
+
point2.set("x", str(target_x))
|
|
288
|
+
point2.set("y", str(target_y))
|
|
289
|
+
point2.set("as", "targetPoint")
|
|
290
|
+
|
|
291
|
+
edge_id += 1
|
|
292
|
+
|
|
293
|
+
# Convert to pretty XML
|
|
294
|
+
rough_string = ET.tostring(root, "utf-8")
|
|
295
|
+
reparsed = minidom.parseString(rough_string)
|
|
296
|
+
pretty_xml = reparsed.toprettyxml(indent=" ")
|
|
297
|
+
|
|
298
|
+
# Remove empty lines
|
|
299
|
+
lines = [line for line in pretty_xml.split("\n") if line.strip()]
|
|
300
|
+
pretty_xml = "\n".join(lines)
|
|
301
|
+
|
|
302
|
+
# Write to file
|
|
303
|
+
with open(output_filename, "w", encoding="utf-8") as f:
|
|
304
|
+
f.write(pretty_xml)
|
|
305
|
+
|
|
306
|
+
print(f"Draw.io file with images created: {output_filename}")
|
|
307
|
+
print(f"Total assets: {len(assets)}")
|
|
308
|
+
print(f"Diagram bounds: x={min_x:.0f} to {max_x:.0f}, y={min_y:.0f} to {max_y:.0f}")
|
|
309
|
+
|
|
310
|
+
# Print asset summary
|
|
311
|
+
type_counts: dict[str, int] = {}
|
|
312
|
+
for asset in assets:
|
|
313
|
+
type_counts[asset.type] = type_counts.get(asset.type, 0) + 1
|
|
314
|
+
|
|
315
|
+
print("\nAsset type distribution:")
|
|
316
|
+
for asset_type, count in sorted(type_counts.items()):
|
|
317
|
+
print(f" {asset_type}: {count}")
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from maltoolbox.model import Model
|
|
2
|
+
|
|
3
|
+
def position_assets(model: Model):
|
|
4
|
+
"""
|
|
5
|
+
Assigns (x, y) positions to assets in a graph where relations are stored
|
|
6
|
+
in asset.associated_assets[relation_name] = [related_assets...].
|
|
7
|
+
Positions are stored in asset.extras['position'] = {'x': ..., 'y': ...}.
|
|
8
|
+
Layout is computed by traversing connected components.
|
|
9
|
+
Adds uniform padding between assets.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
visited = set()
|
|
13
|
+
x_spacing = 200
|
|
14
|
+
y_spacing = 200
|
|
15
|
+
padding = 50 # uniform padding
|
|
16
|
+
|
|
17
|
+
def traverse(asset, depth, index, component):
|
|
18
|
+
if asset in visited:
|
|
19
|
+
return
|
|
20
|
+
visited.add(asset)
|
|
21
|
+
component.append((asset, depth, index))
|
|
22
|
+
neighbors = []
|
|
23
|
+
for rel_list in asset.associated_assets.values():
|
|
24
|
+
neighbors.extend(rel_list)
|
|
25
|
+
for i, neighbor in enumerate(neighbors):
|
|
26
|
+
if neighbor not in visited:
|
|
27
|
+
traverse(neighbor, depth + 1, i, component)
|
|
28
|
+
|
|
29
|
+
def assign_positions(component):
|
|
30
|
+
for i, (asset, depth, index) in enumerate(component):
|
|
31
|
+
x = index * (x_spacing + padding)
|
|
32
|
+
y = depth * (y_spacing + padding)
|
|
33
|
+
asset.extras.setdefault('position', {})
|
|
34
|
+
asset.extras['position']['x'] = x
|
|
35
|
+
asset.extras['position']['y'] = y
|
|
36
|
+
|
|
37
|
+
for asset in model.assets.values():
|
|
38
|
+
if asset not in visited:
|
|
39
|
+
component: list[tuple] = []
|
|
40
|
+
traverse(asset, 0, 0, component)
|
|
41
|
+
assign_positions(component)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|