toolsaf 0.2.0__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.
Files changed (69) hide show
  1. toolsaf/__init__.py +0 -0
  2. toolsaf/adapters/__init__.py +0 -0
  3. toolsaf/adapters/android_manifest_scan.py +89 -0
  4. toolsaf/adapters/batch_import.py +297 -0
  5. toolsaf/adapters/censys_scan.py +78 -0
  6. toolsaf/adapters/certmitm_reader.py +71 -0
  7. toolsaf/adapters/data/android_permissions.json +367 -0
  8. toolsaf/adapters/github_releases.py +57 -0
  9. toolsaf/adapters/har_scan.py +117 -0
  10. toolsaf/adapters/nmap_scan.py +129 -0
  11. toolsaf/adapters/pcap_reader.py +251 -0
  12. toolsaf/adapters/ping_command.py +59 -0
  13. toolsaf/adapters/setup_reader.py +44 -0
  14. toolsaf/adapters/shodan_scan.py +231 -0
  15. toolsaf/adapters/spdx_reader.py +80 -0
  16. toolsaf/adapters/ssh_audit_scan.py +74 -0
  17. toolsaf/adapters/testsslsh_scan.py +73 -0
  18. toolsaf/adapters/tool_finder.py +104 -0
  19. toolsaf/adapters/tools.py +237 -0
  20. toolsaf/adapters/tshark_reader.py +71 -0
  21. toolsaf/adapters/vulnerability_reader.py +46 -0
  22. toolsaf/adapters/web_checker.py +82 -0
  23. toolsaf/adapters/zed_reader.py +64 -0
  24. toolsaf/builder_backend.py +1203 -0
  25. toolsaf/common/__init__.py +0 -0
  26. toolsaf/common/address.py +673 -0
  27. toolsaf/common/android.py +67 -0
  28. toolsaf/common/basics.py +37 -0
  29. toolsaf/common/entity.py +132 -0
  30. toolsaf/common/property.py +295 -0
  31. toolsaf/common/release_info.py +37 -0
  32. toolsaf/common/serializer/__init__.py +0 -0
  33. toolsaf/common/serializer/serializer.py +303 -0
  34. toolsaf/common/traffic.py +523 -0
  35. toolsaf/common/verdict.py +56 -0
  36. toolsaf/core/__init__.py +0 -0
  37. toolsaf/core/components.py +165 -0
  38. toolsaf/core/entity_database.py +89 -0
  39. toolsaf/core/entity_selector.py +32 -0
  40. toolsaf/core/event_interface.py +182 -0
  41. toolsaf/core/event_logger.py +254 -0
  42. toolsaf/core/ignore_rules.py +61 -0
  43. toolsaf/core/inspector.py +276 -0
  44. toolsaf/core/main_tools.py +106 -0
  45. toolsaf/core/matcher.py +620 -0
  46. toolsaf/core/model.py +920 -0
  47. toolsaf/core/online_resources.py +10 -0
  48. toolsaf/core/registry.py +105 -0
  49. toolsaf/core/result.py +354 -0
  50. toolsaf/core/selector.py +425 -0
  51. toolsaf/core/serializer/__init__.py +0 -0
  52. toolsaf/core/serializer/event_serializers.py +421 -0
  53. toolsaf/core/serializer/model_serializers.py +416 -0
  54. toolsaf/core/services.py +93 -0
  55. toolsaf/core/uploader.py +133 -0
  56. toolsaf/diagram_visualizer/backend.png +0 -0
  57. toolsaf/diagram_visualizer/browser.png +0 -0
  58. toolsaf/diagram_visualizer/device.png +0 -0
  59. toolsaf/diagram_visualizer/device_with_multicast.png +0 -0
  60. toolsaf/diagram_visualizer/mobile.png +0 -0
  61. toolsaf/diagram_visualizer/mobile_with_multicast.png +0 -0
  62. toolsaf/diagram_visualizer/multicast.png +0 -0
  63. toolsaf/diagram_visualizer.py +189 -0
  64. toolsaf/main.py +495 -0
  65. toolsaf-0.2.0.dist-info/METADATA +45 -0
  66. toolsaf-0.2.0.dist-info/RECORD +69 -0
  67. toolsaf-0.2.0.dist-info/WHEEL +5 -0
  68. toolsaf-0.2.0.dist-info/licenses/LICENSE +21 -0
  69. toolsaf-0.2.0.dist-info/top_level.txt +1 -0
toolsaf/__init__.py ADDED
File without changes
File without changes
@@ -0,0 +1,89 @@
1
+ """Tool to read Android manifest XML"""
2
+
3
+ import json
4
+ from io import BufferedReader
5
+ from pathlib import Path
6
+ from xml.etree import ElementTree
7
+ from typing import Dict, List, cast
8
+
9
+ from toolsaf.main import ConfigurationException
10
+ from toolsaf.common.basics import HostType
11
+ from toolsaf.common.address import AnyAddress
12
+ from toolsaf.core.components import Software
13
+ from toolsaf.core.event_interface import PropertyEvent, EventInterface
14
+ from toolsaf.core.model import IoTSystem
15
+ from toolsaf.common.property import Properties, PropertyKey
16
+ from toolsaf.adapters.tools import EndpointTool
17
+ from toolsaf.common.traffic import EvidenceSource, Evidence
18
+ from toolsaf.common.verdict import Verdict
19
+ from toolsaf.common.android import MobilePermissions
20
+
21
+
22
+ class AndroidManifestScan(EndpointTool):
23
+ """Android manifest XML tool"""
24
+ def __init__(self, system: IoTSystem) -> None:
25
+ super().__init__("android", ".xml", system)
26
+ self.tool.name = "Android Manifest"
27
+ self.categories = self.load_categories()
28
+
29
+ def load_categories(self) -> Dict[str, List[str]]:
30
+ """Load our Android permission category info from json"""
31
+ data_json_path = Path(__file__).parent / "data/android_permissions.json"
32
+ with open(data_json_path, "r", encoding="utf-8") as f:
33
+ r = json.load(f)
34
+ return cast(Dict[str, List[str]], r)
35
+
36
+ def process_endpoint(self, endpoint: AnyAddress, stream: BufferedReader, interface: EventInterface,
37
+ source: EvidenceSource) -> None:
38
+ node = self.system.get_endpoint(endpoint)
39
+ if node.host_type != HostType.MOBILE:
40
+ raise ConfigurationException(f"Endpoint {endpoint} is not a Mobile application!")
41
+
42
+ if len(all_software := Software.list_software(node)) != 1:
43
+ raise ConfigurationException(
44
+ f"Endpoint {endpoint} needs to have 1 SW component only. Current number is {len(all_software)}!")
45
+ software = all_software[0]
46
+
47
+ evidence = Evidence(source)
48
+
49
+ tree = ElementTree.parse(stream)
50
+ key_set = set()
51
+ for uses_p in tree.getroot().iter('uses-permission'):
52
+ name = str(uses_p.attrib.get("{http://schemas.android.com/apk/res/android}name"))
53
+ if "." in name:
54
+ name = name[name.rindex(".") + 1:]
55
+
56
+ category = self.link_permission_to_category(name)
57
+ key = PropertyKey("permission", category.value)
58
+ key_set.add(key)
59
+
60
+ if self.load_baseline:
61
+ software.permissions.add(category.value)
62
+ ver = Verdict.PASS
63
+ else:
64
+ ver = Verdict.PASS if category.value in software.permissions else Verdict.FAIL
65
+
66
+ if self.send_events:
67
+ ev = PropertyEvent(evidence, software, key.verdict(ver))
68
+ interface.property_update(ev)
69
+
70
+ # Set verdict for permissions that were only present in the statement
71
+ for permission in software.permissions:
72
+ key = PropertyKey("permission", permission)
73
+ if key not in key_set:
74
+ key_set.add(key)
75
+ ver = Verdict.FAIL if not self.load_baseline else Verdict.PASS
76
+ ev = PropertyEvent(evidence, software, key.verdict(ver))
77
+ interface.property_update(ev)
78
+
79
+ if self.send_events:
80
+ ev = PropertyEvent(evidence, software, Properties.PERMISSIONS.value_set(key_set, self.tool.name))
81
+ interface.property_update(ev)
82
+
83
+ def link_permission_to_category(self, permission: str) -> MobilePermissions:
84
+ """Connect given permission to one of our categories.
85
+ If no category is found, uncategorized is returned by default."""
86
+ for category, permissions in self.categories.items():
87
+ if permission in permissions:
88
+ return MobilePermissions(category)
89
+ return MobilePermissions.UNCATEGORIZED
@@ -0,0 +1,297 @@
1
+ """Batch tool-data import"""
2
+
3
+ from datetime import datetime
4
+ import json
5
+ import logging
6
+ import pathlib
7
+ from io import BufferedReader
8
+ from typing import Any, Dict, List, Optional, Set
9
+
10
+ from toolsaf.common.address import Addresses, AnyAddress
11
+ from toolsaf.common.basics import ExternalActivity
12
+ from toolsaf.core.event_interface import EventInterface
13
+ from toolsaf.core.model import Addressable, EvidenceNetworkSource, IoTSystem, NetworkNode
14
+ from toolsaf.adapters.tool_finder import ToolDepiction, TOOL_FINDER
15
+ from toolsaf.common.traffic import EvidenceSource
16
+
17
+
18
+ class BatchImporter:
19
+ """Batch importer for importing a batch of files from a directory."""
20
+ def __init__(self, interface: EventInterface, label_filter: Optional['LabelFilter'] = None,
21
+ load_baseline: bool=False) -> None:
22
+ self.interface = interface
23
+ self.system = interface.get_system()
24
+ self.label_filter = label_filter or LabelFilter()
25
+ self.logger = logging.getLogger("batch_importer")
26
+ self.load_baseline = load_baseline # True to load baseline, false to check it
27
+ self.meta_file_count = 0
28
+
29
+ # collect evidence sources from visited tools
30
+ self.evidence: Dict[str, List[EvidenceSource]] = {}
31
+ # store batch hierarchy
32
+ self.batch_data: List[BatchData] = []
33
+
34
+ def import_batch(self, file: pathlib.Path) -> None:
35
+ """Import a batch of files from a directory or zip file recursively."""
36
+ if file.is_dir():
37
+ bd = BatchData(FileMetaInfo())
38
+ self._import_batch(file, bd)
39
+ if not self.meta_file_count:
40
+ self.logger.warning("No 00meta.json files found")
41
+ self.batch_data.append(bd)
42
+ else:
43
+ raise ValueError(f"Expected directory, got {file.as_posix()}")
44
+
45
+ def _import_batch(self, file: pathlib.Path, parent: 'BatchData') -> None:
46
+ """Import a batch of files from a directory or zip file recursively."""
47
+ parent_info = parent.meta_info
48
+ self.logger.info("scanning %s", file.as_posix())
49
+ if file.is_dir():
50
+ dir_name = file.name
51
+ meta_file = file / "00meta.json"
52
+ if meta_file.is_file():
53
+ # the directory has data files
54
+ if meta_file.stat().st_size == 0:
55
+ info = FileMetaInfo(dir_name, parent=parent_info) # meta_file is empty
56
+ b_data = BatchData(info)
57
+ else:
58
+ try:
59
+ with meta_file.open("rb") as f:
60
+ b_data = BatchData.parse_from_stream(f, dir_name, self.system, parent_meta=parent_info)
61
+ info = b_data.meta_info
62
+ except Exception as e:
63
+ raise ValueError(f"Error in {meta_file.as_posix()}") from e
64
+ self.evidence.setdefault(info.label, [])
65
+ self.meta_file_count += 1
66
+ else:
67
+ info = FileMetaInfo(parent=parent_info)
68
+ b_data = BatchData(info)
69
+ parent.sub_data.append(b_data)
70
+
71
+ # get tool info by file type
72
+ tool_dep = TOOL_FINDER.by_file_type(info.file_type)
73
+
74
+ # list files/directories to process, files first
75
+ proc_list = []
76
+ for a_file in sorted(file.iterdir(), key=lambda f: (f.is_dir(), f.name)):
77
+ if a_file == meta_file:
78
+ continue
79
+ prefix = a_file.name[:1]
80
+ if prefix in {".", "_"}:
81
+ continue
82
+ postfix = a_file.name[-1:]
83
+ if postfix in {"~"}:
84
+ continue
85
+ proc_list.append(a_file)
86
+
87
+ # sort files to specified order, if any
88
+ if info.file_load_order:
89
+ proc_list = FileMetaInfo.sort_load_order(proc_list, info.file_load_order)
90
+
91
+ # filter by label
92
+ skip_processing = not self.label_filter.filter(info.label)
93
+
94
+ # give all files to the tool
95
+ all_files = tool_dep.filter_files_itself()
96
+ if all_files:
97
+ # process all files by one tool
98
+ self._do_process_files(proc_list, b_data, tool_dep, skip_processing)
99
+
100
+ if not info.label:
101
+ self.logger.info("skipping all files as no 00meta.json")
102
+
103
+ # recursively scan the directory
104
+ for a_file in proc_list:
105
+ if info and a_file.is_file():
106
+ if all_files or not info.label:
107
+ continue
108
+ # process the files individually
109
+ if not info.default_include and info.label not in self.label_filter.included:
110
+ self.logger.debug("skipping (default=False) %s", a_file.as_posix())
111
+ continue # skip file if not explicitly included
112
+ with a_file.open("rb") as f:
113
+ self._do_process(f, a_file, b_data, tool_dep, skip_processing)
114
+ else:
115
+ self._import_batch(a_file, b_data)
116
+
117
+ def _do_process(self, stream: BufferedReader, file_path: pathlib.Path, data: 'BatchData', tool: ToolDepiction,
118
+ skip_processing: bool) -> None:
119
+ """Process a file """
120
+ info = data.meta_info
121
+ if not skip_processing:
122
+ self.logger.info("processing (%s) %s", info.label, file_path.as_posix())
123
+
124
+ file_name = file_path.name
125
+ file_ext = file_path.suffix.lower()
126
+ reader = tool.create_tool(self.system, "" if info.from_pipe else file_ext)
127
+
128
+ try:
129
+ if reader:
130
+ ev = info.source.rename(name=reader.tool.name, base_ref=file_path.as_posix(),
131
+ label=info.label)
132
+ # tool-specific code can override, if knows better
133
+ ev.timestamp = datetime.fromtimestamp(file_path.stat().st_mtime)
134
+ self.evidence.setdefault(info.label, []).append(ev)
135
+ if skip_processing:
136
+ self.logger.info("skipping (%s) %s", info.label, file_path.as_posix())
137
+ return
138
+ reader.load_baseline = info.load_baseline or self.load_baseline
139
+ reader.process_file(stream, file_name, self.interface, ev)
140
+ data.sources.append(ev)
141
+ return
142
+
143
+ except Exception as e:
144
+ raise ValueError(f"Error in {file_name}") from e
145
+ self.logger.info("skipping unsupported '%s' type %s", file_name, info.file_type)
146
+
147
+ def _do_process_files(self, files: List[pathlib.Path], data: 'BatchData', tool: ToolDepiction,
148
+ skip_processing: bool) -> None:
149
+ """Process files"""
150
+ info = data.meta_info
151
+ reader = tool.create_tool(self.system)
152
+ if not reader:
153
+ return
154
+ reader.load_baseline = info.load_baseline or self.load_baseline
155
+
156
+ if skip_processing:
157
+ self.logger.info("skipping (%s) data files", info.label)
158
+ ev = info.source.rename(name=reader.tool.name)
159
+ self.evidence.setdefault(info.label, []).append(ev)
160
+ return
161
+
162
+ unmapped = reader.get_processed_files()
163
+ for fn in files:
164
+ if not fn.is_file():
165
+ continue # directories called later
166
+ ev = info.source.rename(name=reader.tool.name, base_ref=fn.as_posix(), label=info.label)
167
+ self.evidence.setdefault(info.label, []).append(ev)
168
+ with fn.open("rb") as f:
169
+ # tool-specific code can override, if knows better
170
+ ev.timestamp = datetime.fromtimestamp(fn.stat().st_mtime)
171
+ done = reader.process_file(f, fn.name, self.interface, ev)
172
+ if done:
173
+ data.sources.append(ev)
174
+ unmapped.remove(fn.name)
175
+ else:
176
+ self.logger.info("unprocessed (%s) file %s", info.label, fn.as_posix())
177
+ if unmapped:
178
+ self.logger.debug("no files for %s", sorted(unmapped))
179
+
180
+
181
+ class FileMetaInfo:
182
+ """Batch file information."""
183
+ def __init__(self, label: str="", file_type: str="", parent: Optional['FileMetaInfo'] = None) -> None:
184
+ self.label = label
185
+ self.name = label
186
+ self.file_load_order: List[str] = []
187
+ self.file_type = file_type
188
+ self.from_pipe = False
189
+ self.load_baseline = False
190
+ self.default_include = True
191
+ self.source = EvidenceNetworkSource(file_type)
192
+ if parent:
193
+ self.source.address_map.update(parent.source.address_map)
194
+ self.source.activity_map.update(parent.source.activity_map)
195
+
196
+ @classmethod
197
+ def sort_load_order(cls, files: List[pathlib.Path], load_order: List[str]) -> List[pathlib.Path]:
198
+ """Sort files according to load order"""
199
+ proc_files = {f.name: f for f in files}
200
+ sorted_files = []
201
+ for fn in load_order:
202
+ if fn in proc_files:
203
+ sorted_files.append(proc_files[fn])
204
+ del proc_files[fn]
205
+ sorted_files.extend(proc_files.values())
206
+ return sorted_files
207
+
208
+ def __repr__(self) -> str:
209
+ return f"{self.name}: file_type: {self.file_type}, label: {self.label}"
210
+
211
+
212
+ class BatchData:
213
+ """Batch data hierarchy"""
214
+ def __init__(self, meta_info: FileMetaInfo):
215
+ self.meta_info = meta_info
216
+ self.sub_data: List[BatchData] = []
217
+ self.address_map: Dict[AnyAddress, Addressable] = {}
218
+ self.activity_map: Dict[NetworkNode, ExternalActivity] = {}
219
+ self.sources: List[EvidenceSource] = []
220
+
221
+ def __repr__(self) -> str:
222
+ return str(self.meta_info)
223
+
224
+ @classmethod
225
+ def parse_from_stream(cls, stream: BufferedReader, directory_name: str, system: IoTSystem,
226
+ parent_meta: Optional['FileMetaInfo'] = None) -> 'BatchData':
227
+ """Parse from stream"""
228
+ return cls.parse_from_json(json.load(stream), directory_name, system, parent_meta)
229
+
230
+ @classmethod
231
+ def parse_from_json(cls, json_data: Dict[str, Any], directory_name: str, system: IoTSystem,
232
+ parent_meta: Optional['FileMetaInfo'] = None) -> 'BatchData':
233
+ """Parse from JSON"""
234
+ label = str(json_data.get("label", directory_name))
235
+ file_type = json_data.get("file_type", "")
236
+ meta_name = json_data.get("name", label)
237
+ info = FileMetaInfo(label, file_type, parent=parent_meta)
238
+ info.name = meta_name
239
+ info.from_pipe = bool(json_data.get("from_pipe", False))
240
+ info.load_baseline = bool(json_data.get("load_baseline", False))
241
+ info.file_load_order = json_data.get("file_order", [])
242
+ info.default_include = bool(json_data.get("include", True))
243
+
244
+ data = cls(info)
245
+
246
+ # read batch-specific addresses
247
+ for add, ent_s in json_data.get("addresses", {}).items():
248
+ address = Addresses.parse_address(add)
249
+ ent = Addresses.parse_address(ent_s)
250
+ entity = system.find_endpoint(ent)
251
+ if not isinstance(entity, Addressable):
252
+ raise ValueError(f"Unknown entity {ent_s}")
253
+ data.address_map[address] = entity
254
+ info.source.address_map[address] = entity
255
+
256
+ # read batch-specific external activity policies
257
+ for ent_s, policy_n in json_data.get("external_activity", {}).items():
258
+ ent = Addresses.parse_address(ent_s)
259
+ node = system.find_endpoint(ent)
260
+ if not isinstance(node, NetworkNode):
261
+ raise ValueError(f"Unknown entity '{ent_s}'")
262
+ policy = ExternalActivity[policy_n]
263
+ data.activity_map[node] = policy
264
+ info.source.activity_map[node] = policy
265
+ return data
266
+
267
+
268
+ class LabelFilter:
269
+ """Filter labels"""
270
+ def __init__(self, label_specification: str="") -> None:
271
+ """Initialize the filter"""
272
+ self.explicit_include = True
273
+ self.included: Set[str] = set()
274
+ self.excluded: Set[str] = set()
275
+ spec = label_specification.strip()
276
+ if spec == "":
277
+ self.explicit_include = False
278
+ return # all included
279
+ for index, d in enumerate(spec.split(",")):
280
+ remove = d.startswith("^")
281
+ if remove:
282
+ # remove label
283
+ if index == 0:
284
+ self.explicit_include = False
285
+ self.excluded.add(d[1:])
286
+ else:
287
+ # include label
288
+ self.included.add(d)
289
+ intersect = self.included.intersection(self.excluded)
290
+ if intersect:
291
+ raise ValueError(f"Labels in both included and excluded: {intersect}")
292
+
293
+ def filter(self, label: str) -> bool:
294
+ """Filter the label"""
295
+ if self.explicit_include:
296
+ return label in self.included
297
+ return label not in self.excluded
@@ -0,0 +1,78 @@
1
+ """Censys scan result tool"""
2
+
3
+ import argparse
4
+ from io import BufferedReader
5
+ import json
6
+ import logging
7
+ import pathlib
8
+
9
+ from censys.search import CensysHosts
10
+
11
+ from toolsaf.common.address import Protocol, EndpointAddress, AnyAddress
12
+ from toolsaf.core.event_interface import PropertyAddressEvent, EventInterface
13
+ from toolsaf.core.model import IoTSystem, NetworkNode, Host
14
+ from toolsaf.common.property import Properties
15
+ from toolsaf.adapters.tools import EndpointTool
16
+ from toolsaf.common.traffic import EvidenceSource, ServiceScan, Evidence, HostScan
17
+ from toolsaf.common.verdict import Verdict
18
+
19
+
20
+ class CensysScan(EndpointTool):
21
+ """Censys scan tool"""
22
+ def __init__(self, system: IoTSystem) -> None:
23
+ super().__init__("censys", ".json", system)
24
+ self.tool.name = "Censys"
25
+
26
+ def filter_node(self, node: NetworkNode) -> bool:
27
+ return isinstance(node, Host)
28
+
29
+ def process_endpoint(self, endpoint: AnyAddress, stream: BufferedReader, interface: EventInterface,
30
+ source: EvidenceSource) -> None:
31
+ raw = json.load(stream)
32
+
33
+ evidence = Evidence(source)
34
+
35
+ host_services = set()
36
+ for s in raw.get('services', []):
37
+ service_name = s.get('service_name', '')
38
+ protocol = Protocol.get_protocol(service_name.upper())
39
+ transport = Protocol.protocol(s.get('transport_protocol'), Protocol.ANY)
40
+ port = int(s['port'])
41
+
42
+ self.logger.info("%s %s %d: %s", endpoint, transport, port, service_name)
43
+ if service_name == "UNKNOWN":
44
+ service_name = ""
45
+ if service_name:
46
+ service_name = f"{service_name} in port {port}"
47
+ elif transport:
48
+ service_name = f"{transport.value} {port}"
49
+ addr = EndpointAddress(endpoint, transport, port)
50
+ interface.service_scan(ServiceScan(evidence, addr, service_name))
51
+
52
+ if protocol == Protocol.HTTP:
53
+ status_code = s.get('http', {}).get('response', {}).get('status_code')
54
+ if status_code == 301:
55
+ # 301 Permanently Moved
56
+ txt = f"{status_code} Permanently Moved"
57
+ ev = PropertyAddressEvent(evidence, addr, Properties.HTTP_REDIRECT.verdict(Verdict.PASS, txt))
58
+ interface.property_address_update(ev)
59
+ host_services.add(addr)
60
+ # other services were not seen
61
+ interface.host_scan(HostScan(evidence, endpoint, host_services))
62
+
63
+
64
+ if __name__ == "__main__":
65
+ arg_parser = argparse.ArgumentParser()
66
+ arg_parser.add_argument("--base-dir", default="censys", help="Base dir to create files into")
67
+ arg_parser.add_argument("addresses", nargs="*", help="Address to resolve using from Censys service")
68
+ args = arg_parser.parse_args()
69
+ logging.basicConfig(format='%(message)s', level='INFO')
70
+ base_dir = pathlib.Path(args.base_dir)
71
+
72
+ m = CensysHosts()
73
+ for a in args.addresses or []:
74
+ save_file = base_dir / f"{a}.json"
75
+ print(f"Scan and save {save_file.as_posix()}")
76
+ info = m.view(a)
77
+ with save_file.open("w") as f:
78
+ json.dump(info, f, indent=4, sort_keys=True)
@@ -0,0 +1,71 @@
1
+ """Certmitm reader"""
2
+
3
+ import json
4
+ from zipfile import ZipFile
5
+ from io import BufferedReader
6
+ from typing import Set, Tuple, Dict, Any, cast
7
+
8
+ from toolsaf.common.address import HWAddresses, DNSName, Protocol
9
+ from toolsaf.core.event_interface import EventInterface, PropertyEvent
10
+ from toolsaf.adapters.tools import SystemWideTool
11
+ from toolsaf.core.model import IoTSystem, Host, Service
12
+ from toolsaf.common.traffic import EvidenceSource, Evidence, IPFlow
13
+ from toolsaf.common.property import PropertyKey
14
+ from toolsaf.common.verdict import Verdict
15
+
16
+
17
+ class CertMITMReader(SystemWideTool):
18
+ """Read MITM logs created by certmitm"""
19
+ def __init__(self, system: IoTSystem) -> None:
20
+ super().__init__("certmitm", system)
21
+ self.tool.name = "certmitm tool"
22
+ self.data_file_suffix = ".zip"
23
+
24
+ def process_file(self, data: BufferedReader, file_name: str, interface: EventInterface,
25
+ source: EvidenceSource) -> bool:
26
+ """Read log file"""
27
+ evidence = Evidence(source)
28
+ connections: Set[Tuple[str, str, str]] = set()
29
+ dns_names: Set[DNSName] = set()
30
+
31
+ # certmitm stores found issues in JSON format to errors.txt
32
+ with ZipFile(data) as zip_file:
33
+ for file in zip_file.filelist:
34
+ if "errors.txt" in file.filename:
35
+ with zip_file.open(file.filename) as error_file:
36
+ for conn_str in error_file.read().decode("utf-8").rstrip().split("\n"):
37
+ conn_json = cast(Dict[str, Any], json.loads(conn_str))
38
+ connections.add(
39
+ (conn_json['client'], conn_json['destination']['ip'], conn_json['destination']['port'])
40
+ )
41
+
42
+ for connection in connections:
43
+ connection_source, target, port = connection
44
+ flow = IPFlow.tcp_flow(
45
+ HWAddresses.NULL.data, connection_source, 0,
46
+ HWAddresses.NULL.data, target, int(port))
47
+ flow.evidence = evidence
48
+ PropertyKey("certmitm").put_verdict(flow.properties, Verdict.FAIL)
49
+ interface.connection(flow)
50
+
51
+ # Workaround for showing that certmitm was used
52
+ with ZipFile(data) as zip_file:
53
+ for file in zip_file.filelist:
54
+ if "certificates" in file.filename:
55
+ dns_name = DNSName(file.filename.split("/")[-1].split("_")[0])
56
+ if dns_name in dns_names:
57
+ continue
58
+ dns_names.add(dns_name)
59
+ if (endpoint := self.system.find_endpoint(dns_name)):
60
+ if not isinstance(endpoint, Host):
61
+ continue
62
+ for endpoint_connection in endpoint.connections:
63
+ if not isinstance(endpoint_connection.target, Service):
64
+ continue
65
+ key = PropertyKey(self.tool_label)
66
+ if endpoint_connection.target.protocol == Protocol.TLS \
67
+ and key not in endpoint_connection.properties:
68
+ ev = PropertyEvent(evidence, endpoint_connection, key.verdict(Verdict.PASS))
69
+ interface.property_update(ev)
70
+
71
+ return True