mal-toolbox 1.1.1__py3-none-any.whl → 1.1.3__py3-none-any.whl
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.1.1.dist-info → mal_toolbox-1.1.3.dist-info}/METADATA +25 -2
- mal_toolbox-1.1.3.dist-info/RECORD +32 -0
- maltoolbox/__init__.py +6 -7
- maltoolbox/__main__.py +17 -9
- maltoolbox/attackgraph/__init__.py +2 -3
- maltoolbox/attackgraph/attackgraph.py +379 -362
- maltoolbox/attackgraph/node.py +14 -19
- maltoolbox/exceptions.py +7 -10
- maltoolbox/file_utils.py +10 -4
- maltoolbox/language/__init__.py +1 -1
- maltoolbox/language/compiler/__init__.py +4 -4
- maltoolbox/language/compiler/mal_lexer.py +154 -154
- maltoolbox/language/compiler/mal_parser.py +784 -1136
- maltoolbox/language/languagegraph.py +487 -639
- maltoolbox/model.py +64 -77
- maltoolbox/patternfinder/attackgraph_patterns.py +17 -8
- maltoolbox/translators/__init__.py +8 -0
- maltoolbox/translators/networkx.py +42 -0
- maltoolbox/translators/updater.py +18 -25
- maltoolbox/visualization/__init__.py +4 -4
- maltoolbox/visualization/draw_io_utils.py +6 -5
- maltoolbox/visualization/graphviz_utils.py +4 -2
- maltoolbox/visualization/neo4j_utils.py +13 -14
- maltoolbox/visualization/utils.py +2 -3
- mal_toolbox-1.1.1.dist-info/RECORD +0 -32
- maltoolbox/translators/securicad.py +0 -179
- {mal_toolbox-1.1.1.dist-info → mal_toolbox-1.1.3.dist-info}/WHEEL +0 -0
- {mal_toolbox-1.1.1.dist-info → mal_toolbox-1.1.3.dist-info}/entry_points.txt +0 -0
- {mal_toolbox-1.1.1.dist-info → mal_toolbox-1.1.3.dist-info}/licenses/AUTHORS +0 -0
- {mal_toolbox-1.1.1.dist-info → mal_toolbox-1.1.3.dist-info}/licenses/LICENSE +0 -0
- {mal_toolbox-1.1.1.dist-info → mal_toolbox-1.1.3.dist-info}/top_level.txt +0 -0
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import random
|
|
2
|
+
|
|
2
3
|
import graphviz
|
|
3
4
|
|
|
4
|
-
from ..model import Model
|
|
5
5
|
from ..attackgraph import AttackGraph
|
|
6
|
+
from ..model import Model
|
|
6
7
|
|
|
7
8
|
graphviz_bright_colors = [
|
|
8
9
|
"aliceblue", "antiquewhite", "antiquewhite1", "antiquewhite2", "azure", "azure1", "azure2",
|
|
@@ -60,11 +61,12 @@ def render_model(model: Model):
|
|
|
60
61
|
)
|
|
61
62
|
dot.render(directory='.', view=True)
|
|
62
63
|
|
|
64
|
+
|
|
63
65
|
def render_attack_graph(attack_graph: AttackGraph):
|
|
64
66
|
"""Render attack graph graphviz, create pdf and open it"""
|
|
65
67
|
assert attack_graph.model, "Attack graph needs a model"
|
|
66
68
|
dot = graphviz.Graph(attack_graph.model.name)
|
|
67
|
-
dot.graph_attr['nodesep'] = '3.0'
|
|
69
|
+
dot.graph_attr['nodesep'] = '3.0' # Node separation
|
|
68
70
|
dot.graph_attr['ratio'] = 'compress'
|
|
69
71
|
|
|
70
72
|
# Create nodes
|
|
@@ -1,24 +1,24 @@
|
|
|
1
|
-
"""
|
|
2
|
-
MAL-Toolbox Neo4j Ingestor Module
|
|
1
|
+
"""MAL-Toolbox Neo4j Ingestor Module
|
|
3
2
|
"""
|
|
4
3
|
# mypy: ignore-errors
|
|
5
4
|
|
|
6
5
|
import logging
|
|
7
|
-
|
|
8
6
|
from typing import Any
|
|
7
|
+
|
|
9
8
|
from py2neo import Graph, Node, Relationship, Subgraph
|
|
10
9
|
|
|
11
10
|
logger = logging.getLogger(__name__)
|
|
12
11
|
|
|
12
|
+
|
|
13
13
|
def ingest_attack_graph_neo4j(
|
|
14
14
|
graph,
|
|
15
15
|
neo4j_config: dict[str, Any],
|
|
16
16
|
delete: bool = True
|
|
17
17
|
) -> None:
|
|
18
|
-
"""
|
|
19
|
-
Ingest an attack graph into a neo4j database
|
|
18
|
+
"""Ingest an attack graph into a neo4j database
|
|
20
19
|
|
|
21
20
|
Arguments:
|
|
21
|
+
---------
|
|
22
22
|
graph - the attackgraph provided by the atkgraph.py module.
|
|
23
23
|
uri - the URI to a running neo4j instance
|
|
24
24
|
username - the username to login on Neo4J
|
|
@@ -26,8 +26,8 @@ def ingest_attack_graph_neo4j(
|
|
|
26
26
|
dbname - the selected database
|
|
27
27
|
delete - if True, the previous content of the database is deleted
|
|
28
28
|
before ingesting the new attack graph
|
|
29
|
-
"""
|
|
30
29
|
|
|
30
|
+
"""
|
|
31
31
|
uri = neo4j_config.get('uri')
|
|
32
32
|
username = neo4j_config.get('username')
|
|
33
33
|
password = neo4j_config.get('password')
|
|
@@ -43,13 +43,12 @@ def ingest_attack_graph_neo4j(
|
|
|
43
43
|
node_dict = node.to_dict()
|
|
44
44
|
nodes[node.id] = Node(
|
|
45
45
|
node_dict['asset'] if 'asset' in node_dict else node_dict['id'],
|
|
46
|
-
name
|
|
47
|
-
full_name
|
|
48
|
-
type
|
|
49
|
-
ttc
|
|
46
|
+
name=node_dict['name'],
|
|
47
|
+
full_name=node.full_name,
|
|
48
|
+
type=node_dict['type'],
|
|
49
|
+
ttc=str(node_dict['ttc']),
|
|
50
50
|
)
|
|
51
51
|
|
|
52
|
-
|
|
53
52
|
for node in graph.nodes.values():
|
|
54
53
|
for child in node.children:
|
|
55
54
|
rels.append(Relationship(nodes[node.id], nodes[child.id]))
|
|
@@ -66,10 +65,10 @@ def ingest_model_neo4j(
|
|
|
66
65
|
neo4j_config: dict[str, Any],
|
|
67
66
|
delete: bool = True
|
|
68
67
|
) -> None:
|
|
69
|
-
"""
|
|
70
|
-
Ingest an instance model graph into a Neo4J database
|
|
68
|
+
"""Ingest an instance model graph into a Neo4J database
|
|
71
69
|
|
|
72
70
|
Arguments:
|
|
71
|
+
---------
|
|
73
72
|
model - the instance model dictionary as provided by the model.py module
|
|
74
73
|
uri - the URI to a running neo4j instance
|
|
75
74
|
username - the username to login on Neo4J
|
|
@@ -77,8 +76,8 @@ def ingest_model_neo4j(
|
|
|
77
76
|
dbname - the selected database
|
|
78
77
|
delete - if True, the previous content of the database is deleted
|
|
79
78
|
before ingesting the new attack graph
|
|
80
|
-
"""
|
|
81
79
|
|
|
80
|
+
"""
|
|
82
81
|
uri = neo4j_config.get('uri')
|
|
83
82
|
username = neo4j_config.get('username')
|
|
84
83
|
password = neo4j_config.get('password')
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
from maltoolbox.model import Model
|
|
2
2
|
|
|
3
|
+
|
|
3
4
|
def position_assets(model: Model):
|
|
4
|
-
"""
|
|
5
|
-
Assigns (x, y) positions to assets in a graph where relations are stored
|
|
5
|
+
"""Assigns (x, y) positions to assets in a graph where relations are stored
|
|
6
6
|
in asset.associated_assets[relation_name] = [related_assets...].
|
|
7
7
|
Positions are stored in asset.extras['position'] = {'x': ..., 'y': ...}.
|
|
8
8
|
Layout is computed by traversing connected components.
|
|
9
9
|
Adds uniform padding between assets.
|
|
10
10
|
"""
|
|
11
|
-
|
|
12
11
|
visited = set()
|
|
13
12
|
x_spacing = 200
|
|
14
13
|
y_spacing = 200
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
mal_toolbox-1.1.1.dist-info/licenses/AUTHORS,sha256=zxLrLe8EY39WtRKlAY4Oorx4Z2_LHV2ApRvDGZgY7xY,127
|
|
2
|
-
mal_toolbox-1.1.1.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
3
|
-
maltoolbox/__init__.py,sha256=XZ8sSK4zPhqPNVWRIy-JKgipTyX3dx6CpnloZs37xlY,2158
|
|
4
|
-
maltoolbox/__main__.py,sha256=XREFzTuS_hIajSt1aNdzW8a5XFS3lAGrenEBnB_zzjI,3484
|
|
5
|
-
maltoolbox/exceptions.py,sha256=0YjPx2v1yYumZ2o7pVZ1s_jS-GAb3Ng979KEFhROSNY,1399
|
|
6
|
-
maltoolbox/file_utils.py,sha256=fYG3UsvPQcU0ES_WI3nLfuzSZgc0jtE4IAxdMGgs9aA,1876
|
|
7
|
-
maltoolbox/model.py,sha256=0z0S0SH4lkslsAxPNxb7exfmNwBPXM_BK0Oujo899a4,18139
|
|
8
|
-
maltoolbox/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
-
maltoolbox/attackgraph/__init__.py,sha256=m_81AjzwXONdclcW_R7mF2f8p-4DvoSRVfQ3Nyh7fak,298
|
|
10
|
-
maltoolbox/attackgraph/attackgraph.py,sha256=ux6TjBLfAgKEfwzGqNU6D_xDGCzc4UTCrdTh1ADoKeI,27722
|
|
11
|
-
maltoolbox/attackgraph/node.py,sha256=F48FgCeYQFEiAq1HswjOnHImYSSRINZpHDvUZP0FqRk,4265
|
|
12
|
-
maltoolbox/attackgraph/analyzers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
-
maltoolbox/language/__init__.py,sha256=TsTTryEyjChwHN1o5F2BSUlFsAss2N6J0H0-nzvXiD8,489
|
|
14
|
-
maltoolbox/language/languagegraph.py,sha256=fNQRSDE_XKQMGgxZe75kpBbLpqjhDD5guAmmx9nSZR0,72143
|
|
15
|
-
maltoolbox/language/compiler/__init__.py,sha256=JQyAgDwJh1pU7AmuOhd1-d2b2PYXpgMVPtxnav8QHVc,15872
|
|
16
|
-
maltoolbox/language/compiler/mal_lexer.py,sha256=BeifykDAt4PloRASOaLzBgWF35ev_zgD8lXMIsSHykc,12063
|
|
17
|
-
maltoolbox/language/compiler/mal_parser.py,sha256=sUoaE43l2VKg-Dou30mk2wlVS1FvdOREwHNIyFe4IkY,114699
|
|
18
|
-
maltoolbox/patternfinder/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
19
|
-
maltoolbox/patternfinder/attackgraph_patterns.py,sha256=jgW7UG1yBJ08jvuokdpSiHm7jDMg2PTcjfGXZ8UJAnw,5082
|
|
20
|
-
maltoolbox/translators/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
21
|
-
maltoolbox/translators/securicad.py,sha256=F_rndv2JyKxfHAXPwf2RrdiFPnemJVArYUpVsFP6QQk,6997
|
|
22
|
-
maltoolbox/translators/updater.py,sha256=UZPnx22udROiocCcSmtrgUJUupkjktkxl-M7rhBxUPc,8660
|
|
23
|
-
maltoolbox/visualization/__init__.py,sha256=JM-cfT_9wtA5GJOAiS_2jtY5TYsuHOFqgn1cZpmcAeE,349
|
|
24
|
-
maltoolbox/visualization/draw_io_utils.py,sha256=7ZmhG5yTEYLZnMhOY9gfyKOb4Dmu2K9v1IdL6Qy-8i0,14372
|
|
25
|
-
maltoolbox/visualization/graphviz_utils.py,sha256=dfQhPL6Z2hvlMFpThsDr-5tm4Pa22SGHEiXw5ym9JJc,3906
|
|
26
|
-
maltoolbox/visualization/neo4j_utils.py,sha256=FHTkkaTvlrf36RXdVdD65TYtlKLNcna9pCl39d6o8wg,3487
|
|
27
|
-
maltoolbox/visualization/utils.py,sha256=8SrGMUXJPFBjDd2Ujw7jgoR2zqCNFdoNHGmG4_tEhwQ,1485
|
|
28
|
-
mal_toolbox-1.1.1.dist-info/METADATA,sha256=Fcv-ZEhpIHUPmGkmR7_QgfkxCt3fncco7BFVl_J9KAs,5960
|
|
29
|
-
mal_toolbox-1.1.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
30
|
-
mal_toolbox-1.1.1.dist-info/entry_points.txt,sha256=oqby5O6cUP_OHCm70k_iYPA6UlbTBf7se1i3XwdK3uU,56
|
|
31
|
-
mal_toolbox-1.1.1.dist-info/top_level.txt,sha256=phqRVLRKGdSUgRY03mcpi2cmbbDo5YGjkV4gkqHFFcM,11
|
|
32
|
-
mal_toolbox-1.1.1.dist-info/RECORD,,
|
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
MAL-Toolbox securiCAD Translator Module
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
import zipfile
|
|
6
|
-
import json
|
|
7
|
-
import logging
|
|
8
|
-
import xml.etree.ElementTree as ET
|
|
9
|
-
|
|
10
|
-
from typing import Optional
|
|
11
|
-
|
|
12
|
-
from ..model import Model
|
|
13
|
-
from ..language import LanguageGraph
|
|
14
|
-
|
|
15
|
-
logger = logging.getLogger(__name__)
|
|
16
|
-
|
|
17
|
-
# TODO: Update this at some point
|
|
18
|
-
|
|
19
|
-
# def load_model_from_scad_archive(
|
|
20
|
-
# scad_archive: str,
|
|
21
|
-
# lang_graph: LanguageGraph
|
|
22
|
-
# ) -> Optional[Model]:
|
|
23
|
-
# """
|
|
24
|
-
# Reads a '.sCAD' archive generated by securiCAD representing an instance
|
|
25
|
-
# model and loads the information into a maltoobox.model.Model object.
|
|
26
|
-
#
|
|
27
|
-
# Arguments:
|
|
28
|
-
# scad_archive - the path to a '.sCAD' archive
|
|
29
|
-
# lang_graph - a language graph representing the MAL
|
|
30
|
-
# language specification
|
|
31
|
-
#
|
|
32
|
-
# Return:
|
|
33
|
-
# A maltoobox.model.Model object containing the instance model.
|
|
34
|
-
# """
|
|
35
|
-
# with zipfile.ZipFile(scad_archive, 'r') as archive:
|
|
36
|
-
# filelist = archive.namelist()
|
|
37
|
-
# model_file = next(filter(lambda x: ( x[-4:] == '.eom'), filelist))
|
|
38
|
-
# scad_model = archive.read(model_file)
|
|
39
|
-
# root = ET.fromstring(scad_model)
|
|
40
|
-
#
|
|
41
|
-
# instance_model = Model(scad_archive,
|
|
42
|
-
# lang_classes_factory)
|
|
43
|
-
#
|
|
44
|
-
# for child in root.iter('objects'):
|
|
45
|
-
#
|
|
46
|
-
# if logger.isEnabledFor(logging.DEBUG):
|
|
47
|
-
# # Avoid running json.dumps when not in debug
|
|
48
|
-
# logger.debug(
|
|
49
|
-
# 'Loading asset from "%s": \n%s',
|
|
50
|
-
# scad_archive, json.dumps(child.attrib, indent=2)
|
|
51
|
-
# )
|
|
52
|
-
#
|
|
53
|
-
# if child.attrib['metaConcept'] == 'Attacker':
|
|
54
|
-
# attacker_obj_id = int(child.attrib['id'])
|
|
55
|
-
# attacker_at = AttackerAttachment()
|
|
56
|
-
# attacker_at.entry_points = []
|
|
57
|
-
# instance_model.add_attacker(
|
|
58
|
-
# attacker_at,
|
|
59
|
-
# attacker_id = attacker_obj_id
|
|
60
|
-
# )
|
|
61
|
-
# continue
|
|
62
|
-
#
|
|
63
|
-
# if not hasattr(lang_classes_factory.ns,
|
|
64
|
-
# child.attrib['metaConcept']):
|
|
65
|
-
# logger.error(
|
|
66
|
-
# 'Failed to find %s asset in language specification!',
|
|
67
|
-
# child.attrib["metaConcept"]
|
|
68
|
-
# )
|
|
69
|
-
# return None
|
|
70
|
-
# asset = getattr(lang_classes_factory.ns,
|
|
71
|
-
# child.attrib['metaConcept'])(name = child.attrib['name'])
|
|
72
|
-
# asset_id = int(child.attrib['id'])
|
|
73
|
-
# for subchild in child.iter('evidenceAttributes'):
|
|
74
|
-
# defense_name = subchild.attrib['metaConcept']
|
|
75
|
-
# defense_name = defense_name[0].lower() + defense_name[1:]
|
|
76
|
-
# for distrib in subchild.iter('evidenceDistribution'):
|
|
77
|
-
# for d in distrib.iter('parameters'):
|
|
78
|
-
# if 'value' in d.attrib:
|
|
79
|
-
# dist_value = d.attrib['value']
|
|
80
|
-
# setattr(asset, defense_name, float(dist_value))
|
|
81
|
-
# instance_model.add_asset(asset, asset_id)
|
|
82
|
-
#
|
|
83
|
-
# for child in root.iter('associations'):
|
|
84
|
-
# logger.debug(
|
|
85
|
-
# 'Load association ("%s", "%s", "%s", "%s") from %s',
|
|
86
|
-
# child.attrib["sourceObject"], child.attrib["targetObject"],
|
|
87
|
-
# child.attrib["targetProperty"], child.attrib["sourceProperty"],
|
|
88
|
-
# scad_archive
|
|
89
|
-
# )
|
|
90
|
-
# # Note: This is not a bug in the code. The fields and assets are
|
|
91
|
-
# # listed incorrectly in the securiCAD format where the source asset
|
|
92
|
-
# # matches the target field and vice versa.
|
|
93
|
-
# left_id = int(child.attrib['targetObject'])
|
|
94
|
-
# right_id = int(child.attrib['sourceObject'])
|
|
95
|
-
# attacker_id = None
|
|
96
|
-
# if child.attrib['sourceProperty'] == 'firstSteps':
|
|
97
|
-
# attacker_id = right_id
|
|
98
|
-
# target_id = left_id
|
|
99
|
-
# target_prop = child.attrib['targetProperty']
|
|
100
|
-
# elif child.attrib['targetProperty'] == 'firstSteps':
|
|
101
|
-
# attacker_id = left_id
|
|
102
|
-
# target_id = right_id
|
|
103
|
-
# target_prop = child.attrib['sourceProperty']
|
|
104
|
-
#
|
|
105
|
-
# if attacker_id is not None:
|
|
106
|
-
# attacker = instance_model.get_attacker_by_id(attacker_id)
|
|
107
|
-
# if not attacker:
|
|
108
|
-
# logger.error(
|
|
109
|
-
# 'Failed to find attacker with id %s in model!',
|
|
110
|
-
# attacker_id
|
|
111
|
-
# )
|
|
112
|
-
# return None
|
|
113
|
-
# target_asset = instance_model.get_asset_by_id(target_id)
|
|
114
|
-
# if not target_asset:
|
|
115
|
-
# logger.error(
|
|
116
|
-
# 'Failed to find asset with id %s in model!',
|
|
117
|
-
# target_id
|
|
118
|
-
# )
|
|
119
|
-
# return None
|
|
120
|
-
# attacker.entry_points.append((target_asset,
|
|
121
|
-
# [target_prop.split('.')[0]]))
|
|
122
|
-
# continue
|
|
123
|
-
#
|
|
124
|
-
# left_asset = instance_model.get_asset_by_id(left_id)
|
|
125
|
-
# if not left_asset:
|
|
126
|
-
# logger.error(
|
|
127
|
-
# 'Failed to find asset with id %s in model!', left_id
|
|
128
|
-
# )
|
|
129
|
-
# return None
|
|
130
|
-
# right_asset = instance_model.get_asset_by_id(right_id)
|
|
131
|
-
# if not right_asset:
|
|
132
|
-
# logger.error(
|
|
133
|
-
# 'Failed to find asset with id %s in model!', right_id
|
|
134
|
-
# )
|
|
135
|
-
# return None
|
|
136
|
-
#
|
|
137
|
-
# # Note: This is not a bug in the code. The fields and assets are
|
|
138
|
-
# # listed incorrectly in the securiCAD format where the source asset
|
|
139
|
-
# # matches the target field and vice versa.
|
|
140
|
-
# left_field = child.attrib['sourceProperty']
|
|
141
|
-
# right_field = child.attrib['targetProperty']
|
|
142
|
-
# lang_graph_assoc = None
|
|
143
|
-
# for assoc in left_asset.lg_asset.associations:
|
|
144
|
-
# if (assoc.left_field.fieldname == left_field and
|
|
145
|
-
# assoc.right_field.fieldname == right_field) or \
|
|
146
|
-
# (assoc.left_field.fieldname == right_field and
|
|
147
|
-
# assoc.right_field.fieldname == left_field):
|
|
148
|
-
# lang_graph_assoc = assoc
|
|
149
|
-
# break
|
|
150
|
-
#
|
|
151
|
-
# if not lang_graph_assoc:
|
|
152
|
-
# raise LookupError(
|
|
153
|
-
# 'Failed to find ("%s", "%s", "%s", "%s")'
|
|
154
|
-
# 'association in lang specification.' %
|
|
155
|
-
# (left_asset.type, right_asset.type,
|
|
156
|
-
# left_field, right_field)
|
|
157
|
-
# )
|
|
158
|
-
# return None
|
|
159
|
-
#
|
|
160
|
-
# logger.debug('Found "%s" association.', lang_graph_assoc.name)
|
|
161
|
-
# assoc_name = lang_classes_factory.get_association_by_signature(
|
|
162
|
-
# lang_graph_assoc.name,
|
|
163
|
-
# left_asset.type,
|
|
164
|
-
# right_asset.type
|
|
165
|
-
# )
|
|
166
|
-
#
|
|
167
|
-
# if assoc_name is None:
|
|
168
|
-
# logger.error(
|
|
169
|
-
# 'Failed to find association with name \"%s\" in model!',
|
|
170
|
-
# lang_graph_assoc.name
|
|
171
|
-
# )
|
|
172
|
-
# return None
|
|
173
|
-
#
|
|
174
|
-
# assoc = getattr(lang_classes_factory.ns, assoc_name)()
|
|
175
|
-
# setattr(assoc, left_field, [left_asset])
|
|
176
|
-
# setattr(assoc, right_field, [right_asset])
|
|
177
|
-
# instance_model.add_association(assoc)
|
|
178
|
-
#
|
|
179
|
-
# return instance_model
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|