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.
- toolsaf/__init__.py +0 -0
- toolsaf/adapters/__init__.py +0 -0
- toolsaf/adapters/android_manifest_scan.py +89 -0
- toolsaf/adapters/batch_import.py +297 -0
- toolsaf/adapters/censys_scan.py +78 -0
- toolsaf/adapters/certmitm_reader.py +71 -0
- toolsaf/adapters/data/android_permissions.json +367 -0
- toolsaf/adapters/github_releases.py +57 -0
- toolsaf/adapters/har_scan.py +117 -0
- toolsaf/adapters/nmap_scan.py +129 -0
- toolsaf/adapters/pcap_reader.py +251 -0
- toolsaf/adapters/ping_command.py +59 -0
- toolsaf/adapters/setup_reader.py +44 -0
- toolsaf/adapters/shodan_scan.py +231 -0
- toolsaf/adapters/spdx_reader.py +80 -0
- toolsaf/adapters/ssh_audit_scan.py +74 -0
- toolsaf/adapters/testsslsh_scan.py +73 -0
- toolsaf/adapters/tool_finder.py +104 -0
- toolsaf/adapters/tools.py +237 -0
- toolsaf/adapters/tshark_reader.py +71 -0
- toolsaf/adapters/vulnerability_reader.py +46 -0
- toolsaf/adapters/web_checker.py +82 -0
- toolsaf/adapters/zed_reader.py +64 -0
- toolsaf/builder_backend.py +1203 -0
- toolsaf/common/__init__.py +0 -0
- toolsaf/common/address.py +673 -0
- toolsaf/common/android.py +67 -0
- toolsaf/common/basics.py +37 -0
- toolsaf/common/entity.py +132 -0
- toolsaf/common/property.py +295 -0
- toolsaf/common/release_info.py +37 -0
- toolsaf/common/serializer/__init__.py +0 -0
- toolsaf/common/serializer/serializer.py +303 -0
- toolsaf/common/traffic.py +523 -0
- toolsaf/common/verdict.py +56 -0
- toolsaf/core/__init__.py +0 -0
- toolsaf/core/components.py +165 -0
- toolsaf/core/entity_database.py +89 -0
- toolsaf/core/entity_selector.py +32 -0
- toolsaf/core/event_interface.py +182 -0
- toolsaf/core/event_logger.py +254 -0
- toolsaf/core/ignore_rules.py +61 -0
- toolsaf/core/inspector.py +276 -0
- toolsaf/core/main_tools.py +106 -0
- toolsaf/core/matcher.py +620 -0
- toolsaf/core/model.py +920 -0
- toolsaf/core/online_resources.py +10 -0
- toolsaf/core/registry.py +105 -0
- toolsaf/core/result.py +354 -0
- toolsaf/core/selector.py +425 -0
- toolsaf/core/serializer/__init__.py +0 -0
- toolsaf/core/serializer/event_serializers.py +421 -0
- toolsaf/core/serializer/model_serializers.py +416 -0
- toolsaf/core/services.py +93 -0
- toolsaf/core/uploader.py +133 -0
- toolsaf/diagram_visualizer/backend.png +0 -0
- toolsaf/diagram_visualizer/browser.png +0 -0
- toolsaf/diagram_visualizer/device.png +0 -0
- toolsaf/diagram_visualizer/device_with_multicast.png +0 -0
- toolsaf/diagram_visualizer/mobile.png +0 -0
- toolsaf/diagram_visualizer/mobile_with_multicast.png +0 -0
- toolsaf/diagram_visualizer/multicast.png +0 -0
- toolsaf/diagram_visualizer.py +189 -0
- toolsaf/main.py +495 -0
- toolsaf-0.2.0.dist-info/METADATA +45 -0
- toolsaf-0.2.0.dist-info/RECORD +69 -0
- toolsaf-0.2.0.dist-info/WHEEL +5 -0
- toolsaf-0.2.0.dist-info/licenses/LICENSE +21 -0
- 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
|