emfuzzer 0.1.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.
- emfuzzer/__init__.py +62 -0
- emfuzzer/__main__.py +92 -0
- emfuzzer/arguments.py +16 -0
- emfuzzer/case.py +43 -0
- emfuzzer/coap/__init__.py +109 -0
- emfuzzer/coap/code.py +98 -0
- emfuzzer/coap/validator.py +91 -0
- emfuzzer/config.py +86 -0
- emfuzzer/context.py +74 -0
- emfuzzer/delay.py +38 -0
- emfuzzer/injector/__init__.py +50 -0
- emfuzzer/injector/subprocess.py +62 -0
- emfuzzer/injector/subtask.py +30 -0
- emfuzzer/io/__init__.py +227 -0
- emfuzzer/io/net.py +31 -0
- emfuzzer/io/sockets.py +71 -0
- emfuzzer/io/streams.py +91 -0
- emfuzzer/results/__init__.py +109 -0
- emfuzzer/results/basic.py +17 -0
- emfuzzer/ssh/__init__.py +10 -0
- emfuzzer/ssh/connectionconfig.py +29 -0
- emfuzzer/ssh/invoker.py +139 -0
- emfuzzer/ssh/reader.py +93 -0
- emfuzzer/subtasks/__init__.py +130 -0
- emfuzzer/subtasks/ping.py +149 -0
- emfuzzer/subtasks/remote.py +76 -0
- emfuzzer/subtasks/subprocess.py +152 -0
- emfuzzer/subtasks/subtask.py +63 -0
- emfuzzer/version.py +48 -0
- emfuzzer-0.1.0.dist-info/LICENSE.txt +19 -0
- emfuzzer-0.1.0.dist-info/METADATA +270 -0
- emfuzzer-0.1.0.dist-info/RECORD +34 -0
- emfuzzer-0.1.0.dist-info/WHEEL +4 -0
- emfuzzer-0.1.0.dist-info/entry_points.txt +3 -0
emfuzzer/__init__.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Copyright (c) 2025 Warsaw University of Technology
|
|
2
|
+
# This file is licensed under the MIT License.
|
|
3
|
+
# See the LICENSE.txt file in the root of the repository for full details.
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
Main module of the application.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
|
|
12
|
+
from .arguments import Arguments
|
|
13
|
+
from .case import Case
|
|
14
|
+
from .config import Config
|
|
15
|
+
from .context import Context
|
|
16
|
+
from .delay import Delay
|
|
17
|
+
from .injector import Injector
|
|
18
|
+
from .results import Results
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def run(args: Arguments, config: Config) -> int:
|
|
24
|
+
results = Results(config)
|
|
25
|
+
|
|
26
|
+
with Context(config) as context:
|
|
27
|
+
case = Case.from_config(context=context, results=results)
|
|
28
|
+
|
|
29
|
+
injector = Injector.from_config(results=results, context=context)
|
|
30
|
+
|
|
31
|
+
delay_between_cases = Delay.from_config(
|
|
32
|
+
"delays", "between_cases", config=config
|
|
33
|
+
)
|
|
34
|
+
delay_before_inject = Delay.from_config(
|
|
35
|
+
"delays", "before_inject", config=config
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
for path in args.data:
|
|
39
|
+
logger.info(f"Opening {path}")
|
|
40
|
+
with path.open("rb") as file:
|
|
41
|
+
data = file.read()
|
|
42
|
+
if len(data) == 0:
|
|
43
|
+
logger.warning(f"No data found, skipping {path}")
|
|
44
|
+
continue
|
|
45
|
+
|
|
46
|
+
case_name = str(path)
|
|
47
|
+
results.add_key(case_name)
|
|
48
|
+
|
|
49
|
+
with case.execute(case_name):
|
|
50
|
+
delay_before_inject.wait()
|
|
51
|
+
injector.inject(case_name, data)
|
|
52
|
+
|
|
53
|
+
delay_between_cases.wait()
|
|
54
|
+
|
|
55
|
+
results.finish()
|
|
56
|
+
logger.info(f"Results:\n {results.summary()}")
|
|
57
|
+
|
|
58
|
+
with open(args.output_prefix + ".json", "w", encoding="utf-8") as f:
|
|
59
|
+
json.dump(results.to_dict(), f, indent=2)
|
|
60
|
+
f.write("\n")
|
|
61
|
+
|
|
62
|
+
return results.total_errors()
|
emfuzzer/__main__.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Copyright (c) 2025 Warsaw University of Technology
|
|
2
|
+
# This file is licensed under the MIT License.
|
|
3
|
+
# See the LICENSE.txt file in the root of the repository for full details.
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
Main entry point to the application.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import logging
|
|
11
|
+
import sys
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from . import run
|
|
16
|
+
from .arguments import Arguments
|
|
17
|
+
from .config import Config
|
|
18
|
+
from .version import VERSION
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def __parse_data(parser: argparse.ArgumentParser, data: list[str]) -> list[Path]:
|
|
22
|
+
result = [Path(f) for f in data]
|
|
23
|
+
for f in result:
|
|
24
|
+
if not f.is_file():
|
|
25
|
+
parser.error(f"Specified path is not a file: {f}")
|
|
26
|
+
if len(result) != len(set(result)):
|
|
27
|
+
parser.error("Non-unique file names as inputs - results would be inconsistent")
|
|
28
|
+
return result
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def __setup_logger(prefix: str) -> None:
|
|
32
|
+
log_format = "%(asctime)s [%(levelname)8s](%(name)20s): %(message)s"
|
|
33
|
+
logging.basicConfig(
|
|
34
|
+
level=logging.DEBUG,
|
|
35
|
+
format=log_format,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
root_logger = logging.getLogger()
|
|
39
|
+
handler = logging.FileHandler(f"{prefix}.log")
|
|
40
|
+
handler.setFormatter(root_logger.handlers[0].formatter)
|
|
41
|
+
logging.getLogger().addHandler(handler)
|
|
42
|
+
|
|
43
|
+
root_logger.info(f"Started instance ({VERSION})")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def parse_args() -> Arguments:
|
|
47
|
+
parser = argparse.ArgumentParser(
|
|
48
|
+
prog="emfuzzer",
|
|
49
|
+
description="Fuzzing experiments orchestrator for embedded",
|
|
50
|
+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
51
|
+
)
|
|
52
|
+
parser.add_argument(
|
|
53
|
+
"data",
|
|
54
|
+
nargs="+",
|
|
55
|
+
help="list of files containing binary data to send to the target",
|
|
56
|
+
)
|
|
57
|
+
parser.add_argument(
|
|
58
|
+
"--output-prefix",
|
|
59
|
+
help="prefix to be used for saving output (logs, reports, etc.)",
|
|
60
|
+
default="emfuzzer",
|
|
61
|
+
type=str,
|
|
62
|
+
)
|
|
63
|
+
parser.add_argument(
|
|
64
|
+
"--config",
|
|
65
|
+
help="path to the configuration file",
|
|
66
|
+
default="default-config.json",
|
|
67
|
+
type=Path,
|
|
68
|
+
)
|
|
69
|
+
parser.add_argument(
|
|
70
|
+
"--version",
|
|
71
|
+
action="version",
|
|
72
|
+
version=VERSION,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
args = parser.parse_args()
|
|
76
|
+
|
|
77
|
+
args.data = __parse_data(parser, args.data)
|
|
78
|
+
args.output_prefix += f"-{datetime.now():%Y%m%d-%H%M%S}"
|
|
79
|
+
|
|
80
|
+
return args
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def main() -> int:
|
|
84
|
+
args = parse_args()
|
|
85
|
+
|
|
86
|
+
__setup_logger(args.output_prefix)
|
|
87
|
+
|
|
88
|
+
return run(args, Config.from_file(args.config))
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
if __name__ == "__main__":
|
|
92
|
+
sys.exit(main())
|
emfuzzer/arguments.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Copyright (c) 2025 Warsaw University of Technology
|
|
2
|
+
# This file is licensed under the MIT License.
|
|
3
|
+
# See the LICENSE.txt file in the root of the repository for full details.
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
Module representing command line arguments.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Protocol
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Arguments(Protocol): # pylint: disable=too-few-public-methods
|
|
14
|
+
data: list[Path]
|
|
15
|
+
output_prefix: str
|
|
16
|
+
config: Path
|
emfuzzer/case.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Copyright (c) 2025 Warsaw University of Technology
|
|
2
|
+
# This file is licensed under the MIT License.
|
|
3
|
+
# See the LICENSE.txt file in the root of the repository for full details.
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
Module representing "case" - an instance of the experiment execution.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from contextlib import contextmanager
|
|
10
|
+
from typing import Iterator, Self
|
|
11
|
+
|
|
12
|
+
from .context import Context
|
|
13
|
+
from .results import Results
|
|
14
|
+
from .subtasks import SubTasks
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Case:
|
|
18
|
+
|
|
19
|
+
def __init__(self, setups: SubTasks, monitoring: SubTasks, checks: SubTasks):
|
|
20
|
+
self._setups = setups
|
|
21
|
+
self._monitoring = monitoring
|
|
22
|
+
self._checks = checks
|
|
23
|
+
|
|
24
|
+
@contextmanager
|
|
25
|
+
def execute(self, case_name: str) -> Iterator[None]:
|
|
26
|
+
self._setups.execute_for(case_name)
|
|
27
|
+
with self._monitoring.monitor(case_name):
|
|
28
|
+
yield
|
|
29
|
+
self._checks.execute_for(case_name)
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
def from_config(cls, context: Context, results: Results) -> Self:
|
|
33
|
+
return cls(
|
|
34
|
+
setups=SubTasks.from_config(
|
|
35
|
+
"case", "setups", results=results, context=context
|
|
36
|
+
),
|
|
37
|
+
checks=SubTasks.from_config(
|
|
38
|
+
"case", "checks", results=results, context=context
|
|
39
|
+
),
|
|
40
|
+
monitoring=SubTasks.from_config(
|
|
41
|
+
"case", "monitoring", results=results, context=context
|
|
42
|
+
),
|
|
43
|
+
)
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# Copyright (c) 2025 Warsaw University of Technology
|
|
2
|
+
# This file is licensed under the MIT License.
|
|
3
|
+
# See the LICENSE.txt file in the root of the repository for full details.
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
CoAP - Constrained Application Protocol support module.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from enum import StrEnum, auto
|
|
10
|
+
from typing import Self
|
|
11
|
+
|
|
12
|
+
from ..config import Config
|
|
13
|
+
from ..context import Context
|
|
14
|
+
from ..delay import Delay
|
|
15
|
+
from ..injector.subtask import TypedInjectionSubTask
|
|
16
|
+
from ..io import IOLoop, SendQueue
|
|
17
|
+
from ..io.net import NetworkAddress
|
|
18
|
+
from ..io.sockets import UdpClientSocket
|
|
19
|
+
from ..subtasks.subtask import SubTask, TypedSubTask
|
|
20
|
+
from .validator import Validator
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class CoapMonitorResult(StrEnum):
|
|
24
|
+
SUCCESS = auto()
|
|
25
|
+
UNEXPECTED_MESSAGE_RECEIVED = auto()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class CoapMonitor(TypedSubTask[CoapMonitorResult]):
|
|
29
|
+
# pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
name: str,
|
|
33
|
+
io: IOLoop,
|
|
34
|
+
target: NetworkAddress,
|
|
35
|
+
response_timeout: float,
|
|
36
|
+
observation_timeout: float,
|
|
37
|
+
):
|
|
38
|
+
super().__init__(name)
|
|
39
|
+
self._io = io
|
|
40
|
+
self._target = target
|
|
41
|
+
self._response_timeout = response_timeout
|
|
42
|
+
self._delay = Delay(observation_timeout, name + ".observation")
|
|
43
|
+
|
|
44
|
+
self._validator: Validator | None = None
|
|
45
|
+
self._socket: UdpClientSocket | None = None
|
|
46
|
+
self._queue: SendQueue[tuple[NetworkAddress, bytes]] | None = None
|
|
47
|
+
|
|
48
|
+
def start(self) -> CoapMonitorResult | SubTask.StartedType:
|
|
49
|
+
self._queue = self._io.make_queue(tuple[NetworkAddress, bytes])
|
|
50
|
+
self._validator = Validator(self._target, self._response_timeout)
|
|
51
|
+
self._socket = UdpClientSocket(
|
|
52
|
+
self.name() + ".udp", self._queue, self._validator
|
|
53
|
+
)
|
|
54
|
+
self._io.register(self._socket)
|
|
55
|
+
return SubTask.STARTED
|
|
56
|
+
|
|
57
|
+
def finish(self) -> CoapMonitorResult:
|
|
58
|
+
assert self._socket
|
|
59
|
+
assert self._validator
|
|
60
|
+
self._delay.wait()
|
|
61
|
+
self._io.close(self._socket)
|
|
62
|
+
return (
|
|
63
|
+
CoapMonitorResult.SUCCESS
|
|
64
|
+
if self._validator.unexpected_messages == 0
|
|
65
|
+
else CoapMonitorResult.UNEXPECTED_MESSAGE_RECEIVED
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
def result_type(self) -> type[CoapMonitorResult]:
|
|
69
|
+
return CoapMonitorResult
|
|
70
|
+
|
|
71
|
+
def send(self, data: bytes) -> None:
|
|
72
|
+
assert self._queue is not None
|
|
73
|
+
self._queue.put((self._target, data))
|
|
74
|
+
|
|
75
|
+
def wait_for_response(self) -> Validator.Result:
|
|
76
|
+
assert self._validator
|
|
77
|
+
return self._validator.wait_for_result()
|
|
78
|
+
|
|
79
|
+
@classmethod
|
|
80
|
+
def from_config(cls, name: str, config: Config, context: Context) -> Self:
|
|
81
|
+
result = cls(
|
|
82
|
+
name=name,
|
|
83
|
+
target=NetworkAddress.from_config(config.section("target")),
|
|
84
|
+
response_timeout=config.get_float("response_timeout"),
|
|
85
|
+
observation_timeout=config.get_float("observation_timeout"),
|
|
86
|
+
io=context.worker(IOLoop),
|
|
87
|
+
)
|
|
88
|
+
context.register_data(name, result)
|
|
89
|
+
return result
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class CoapInjector(TypedInjectionSubTask[Validator.Result]):
|
|
93
|
+
def __init__(self, name: str, monitor: CoapMonitor):
|
|
94
|
+
super().__init__(name)
|
|
95
|
+
self._monitor = monitor
|
|
96
|
+
|
|
97
|
+
def inject(self, data: bytes) -> Validator.Result:
|
|
98
|
+
self._monitor.send(data)
|
|
99
|
+
return self._monitor.wait_for_response()
|
|
100
|
+
|
|
101
|
+
def result_type(self) -> type[Validator.Result]:
|
|
102
|
+
return Validator.Result
|
|
103
|
+
|
|
104
|
+
@classmethod
|
|
105
|
+
def from_config(cls, name: str, config: Config, context: Context) -> Self:
|
|
106
|
+
return cls(
|
|
107
|
+
name=name,
|
|
108
|
+
monitor=context.data(CoapMonitor, config.get_str("monitor")),
|
|
109
|
+
)
|
emfuzzer/coap/code.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Copyright (c) 2025 Warsaw University of Technology
|
|
2
|
+
# This file is licensed under the MIT License.
|
|
3
|
+
# See the LICENSE.txt file in the root of the repository for full details.
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
CoAP - protocol codes etc.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def decode_code(octet: int) -> tuple[int, int]:
|
|
11
|
+
clazz = (octet & 0b11100000) >> 5
|
|
12
|
+
code = octet & 0xB00011111
|
|
13
|
+
return clazz, code
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def code_short_string(code: tuple[int, int]) -> str:
|
|
17
|
+
return f"{code[0]}.{code[1]:02}"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
__MESSAGES: dict[int, tuple[str, dict[int, str]]] = {
|
|
21
|
+
1: (
|
|
22
|
+
"REQUEST",
|
|
23
|
+
{
|
|
24
|
+
0: "EMPTY",
|
|
25
|
+
1: "GET",
|
|
26
|
+
2: "POST",
|
|
27
|
+
3: "PUT",
|
|
28
|
+
4: "DELETE",
|
|
29
|
+
5: "FETCH",
|
|
30
|
+
6: "PATCH",
|
|
31
|
+
7: "iPATCH",
|
|
32
|
+
},
|
|
33
|
+
),
|
|
34
|
+
2: (
|
|
35
|
+
"SUCCESS",
|
|
36
|
+
{
|
|
37
|
+
1: "Created",
|
|
38
|
+
2: "Deleted",
|
|
39
|
+
3: "Valid",
|
|
40
|
+
4: "Changed",
|
|
41
|
+
5: "Content",
|
|
42
|
+
31: "Continue",
|
|
43
|
+
},
|
|
44
|
+
),
|
|
45
|
+
4: (
|
|
46
|
+
"CLIENT ERROR",
|
|
47
|
+
{
|
|
48
|
+
0: "Bad Request",
|
|
49
|
+
1: "Unauthorized",
|
|
50
|
+
2: "Bad Option",
|
|
51
|
+
3: "Forbidden",
|
|
52
|
+
4: "Not Found",
|
|
53
|
+
5: "Method Not Allowed",
|
|
54
|
+
6: "Not Acceptable",
|
|
55
|
+
8: "Request Entity Incomplete",
|
|
56
|
+
9: "Conflict",
|
|
57
|
+
12: "Precondition Failed",
|
|
58
|
+
13: "Request Entity Too Large",
|
|
59
|
+
15: "Unsupported Content-Format",
|
|
60
|
+
},
|
|
61
|
+
),
|
|
62
|
+
5: (
|
|
63
|
+
"SERVER ERROR",
|
|
64
|
+
{
|
|
65
|
+
0: "Internal server error",
|
|
66
|
+
1: "Not implemented",
|
|
67
|
+
2: "Bad gateway",
|
|
68
|
+
3: "Service unavailable",
|
|
69
|
+
4: "Gateway timeout",
|
|
70
|
+
5: "Proxying not supported",
|
|
71
|
+
},
|
|
72
|
+
),
|
|
73
|
+
7: (
|
|
74
|
+
"SIGNALING",
|
|
75
|
+
{
|
|
76
|
+
0: "Unassigned",
|
|
77
|
+
1: "CSM",
|
|
78
|
+
2: "Ping",
|
|
79
|
+
3: "Pong",
|
|
80
|
+
4: "Release",
|
|
81
|
+
5: "Abort",
|
|
82
|
+
},
|
|
83
|
+
),
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def code_message(code: tuple[int, int]) -> str:
|
|
88
|
+
class_message, code_dict = __MESSAGES.get(code[0], ("UNKNOWN", {}))
|
|
89
|
+
message = code_dict.get(code[1], "-unknown-")
|
|
90
|
+
return f"{class_message}: {message}"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def code_to_string(code: tuple[int, int]) -> str:
|
|
94
|
+
return f"{code_short_string(code)} ({code_message(code)})"
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def code_reports_success(code: tuple[int, int]) -> bool:
|
|
98
|
+
return code[0] == 2
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Copyright (c) 2025 Warsaw University of Technology
|
|
2
|
+
# This file is licensed under the MIT License.
|
|
3
|
+
# See the LICENSE.txt file in the root of the repository for full details.
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
CoAP - communication validator.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import threading
|
|
11
|
+
from enum import StrEnum, auto
|
|
12
|
+
|
|
13
|
+
from ..io.net import NetworkAddress, NetworkObserver
|
|
14
|
+
from .code import code_reports_success, code_to_string, decode_code
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Validator(NetworkObserver):
|
|
20
|
+
|
|
21
|
+
class Result(StrEnum):
|
|
22
|
+
SUCCESS = auto()
|
|
23
|
+
UNKNOWN = auto()
|
|
24
|
+
UNEXPECTED_ORIGIN = auto()
|
|
25
|
+
MESSAGE_TOO_SHORT = auto()
|
|
26
|
+
OPERATION_FAILURE = auto()
|
|
27
|
+
TIMEOUT = auto()
|
|
28
|
+
|
|
29
|
+
def __init__(self, expected_ip: NetworkAddress, timeout: float):
|
|
30
|
+
self.expected_ip = expected_ip
|
|
31
|
+
self.timeout = timeout
|
|
32
|
+
|
|
33
|
+
self.cond = threading.Condition()
|
|
34
|
+
self.expecting = False
|
|
35
|
+
self.result: Validator.Result = self.Result.UNKNOWN
|
|
36
|
+
|
|
37
|
+
self.unexpected_messages = 0
|
|
38
|
+
|
|
39
|
+
def on_read(self, address: NetworkAddress, data: bytes) -> None:
|
|
40
|
+
with self.cond:
|
|
41
|
+
if not self.expecting:
|
|
42
|
+
self.__unexpected_message()
|
|
43
|
+
return
|
|
44
|
+
self.expecting = False
|
|
45
|
+
self.result = self.check_message(address, data)
|
|
46
|
+
self.cond.notify()
|
|
47
|
+
|
|
48
|
+
def check_message(self, address: NetworkAddress, data: bytes) -> Result:
|
|
49
|
+
if address != self.expected_ip:
|
|
50
|
+
logger.warning(
|
|
51
|
+
f"Message received from unexpected origin: {address} vs {self.expected_ip}"
|
|
52
|
+
)
|
|
53
|
+
return self.Result.UNEXPECTED_ORIGIN
|
|
54
|
+
|
|
55
|
+
if len(data) < 2:
|
|
56
|
+
logger.warning("Too short message")
|
|
57
|
+
return self.Result.MESSAGE_TOO_SHORT
|
|
58
|
+
|
|
59
|
+
code = decode_code(data[1])
|
|
60
|
+
|
|
61
|
+
logger.info(f"Received {code_to_string(code)}")
|
|
62
|
+
|
|
63
|
+
if not code_reports_success(code):
|
|
64
|
+
logger.warning("Operation reported as failed")
|
|
65
|
+
return self.Result.OPERATION_FAILURE
|
|
66
|
+
|
|
67
|
+
return self.Result.SUCCESS
|
|
68
|
+
|
|
69
|
+
def on_write(self, address: NetworkAddress, data: bytes) -> None:
|
|
70
|
+
with self.cond:
|
|
71
|
+
self.expecting = True
|
|
72
|
+
self.result = self.Result.UNKNOWN
|
|
73
|
+
|
|
74
|
+
def wait_for_result(self) -> Result:
|
|
75
|
+
with self.cond:
|
|
76
|
+
if not self.cond.wait_for(
|
|
77
|
+
lambda: self.result != self.Result.UNKNOWN, timeout=self.timeout
|
|
78
|
+
):
|
|
79
|
+
self.expecting = False
|
|
80
|
+
logger.warning("Operation timed out")
|
|
81
|
+
return self.Result.TIMEOUT
|
|
82
|
+
result = self.result
|
|
83
|
+
self.result = self.Result.UNKNOWN
|
|
84
|
+
return result
|
|
85
|
+
|
|
86
|
+
def extra_stats(self) -> dict[str, int]:
|
|
87
|
+
return {"unexpected_messages": self.unexpected_messages}
|
|
88
|
+
|
|
89
|
+
def __unexpected_message(self) -> None:
|
|
90
|
+
logger.warning("Message unexpected at this stage")
|
|
91
|
+
self.unexpected_messages += 1
|
emfuzzer/config.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Copyright (c) 2025 Warsaw University of Technology
|
|
2
|
+
# This file is licensed under the MIT License.
|
|
3
|
+
# See the LICENSE.txt file in the root of the repository for full details.
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
Module for loading application configuration.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Self, cast
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Config:
|
|
15
|
+
|
|
16
|
+
def __init__(self, obj: dict[str, Any]):
|
|
17
|
+
self._obj = obj
|
|
18
|
+
|
|
19
|
+
def section(self, path: str, *subpath: str) -> Self:
|
|
20
|
+
subsection = cast(dict[str, Any], self._obj[path])
|
|
21
|
+
config = self.__class__(subsection)
|
|
22
|
+
if subpath:
|
|
23
|
+
try:
|
|
24
|
+
config.section(*subpath)
|
|
25
|
+
except KeyError:
|
|
26
|
+
raise KeyError(path, *subpath) from None
|
|
27
|
+
return config
|
|
28
|
+
|
|
29
|
+
def _get_value(self, path: str, *subpath: str) -> Any:
|
|
30
|
+
if subpath:
|
|
31
|
+
try:
|
|
32
|
+
# pylint: disable=protected-access
|
|
33
|
+
return self.section(path)._get_value(*subpath)
|
|
34
|
+
except KeyError:
|
|
35
|
+
raise KeyError(path, *subpath) from None
|
|
36
|
+
return self._obj[path]
|
|
37
|
+
|
|
38
|
+
def get_int(self, path: str, *subpath: str) -> int:
|
|
39
|
+
value = self._get_value(path, *subpath)
|
|
40
|
+
if not isinstance(value, int):
|
|
41
|
+
raise TypeError("not an int", path, *subpath)
|
|
42
|
+
return value
|
|
43
|
+
|
|
44
|
+
def get_float(self, path: str, *subpath: str) -> float:
|
|
45
|
+
value = self._get_value(path, *subpath)
|
|
46
|
+
if type(value) not in (int, float):
|
|
47
|
+
raise TypeError("not an float", path, *subpath)
|
|
48
|
+
return float(value)
|
|
49
|
+
|
|
50
|
+
def get_bool(self, path: str, *subpath: str) -> bool:
|
|
51
|
+
value = self._get_value(path, *subpath)
|
|
52
|
+
if type(value) not in (int, bool):
|
|
53
|
+
raise TypeError("not an bool", path, *subpath)
|
|
54
|
+
return bool(value)
|
|
55
|
+
|
|
56
|
+
def get_str(self, path: str, *subpath: str) -> str:
|
|
57
|
+
value = self._get_value(path, *subpath)
|
|
58
|
+
if not isinstance(value, str):
|
|
59
|
+
raise TypeError("not an str", path, *subpath)
|
|
60
|
+
return value
|
|
61
|
+
|
|
62
|
+
def get_config_list(self, path: str, *subpath: str) -> list[Self]:
|
|
63
|
+
value = self._get_value(path, *subpath)
|
|
64
|
+
if not isinstance(value, list):
|
|
65
|
+
raise TypeError("not an list", path, *subpath)
|
|
66
|
+
if any(not isinstance(x, dict) for x in value):
|
|
67
|
+
raise TypeError("not all elements are dict", path, *subpath)
|
|
68
|
+
return [self.__class__(v) for v in value]
|
|
69
|
+
|
|
70
|
+
def get_str_list(self, path: str, *subpath: str) -> list[str]:
|
|
71
|
+
value = self._get_value(path, *subpath)
|
|
72
|
+
if not isinstance(value, list):
|
|
73
|
+
raise TypeError("not an list", path, *subpath)
|
|
74
|
+
if any(not isinstance(x, str) for x in value):
|
|
75
|
+
raise TypeError("not all elements are str", path, *subpath)
|
|
76
|
+
return value
|
|
77
|
+
|
|
78
|
+
def to_dict(self) -> dict[str, Any]:
|
|
79
|
+
return self._obj
|
|
80
|
+
|
|
81
|
+
@classmethod
|
|
82
|
+
def from_file(cls, path: Path) -> Self:
|
|
83
|
+
with path.open() as file:
|
|
84
|
+
config = cls(json.load(file))
|
|
85
|
+
config._obj["__path__"] = str(path)
|
|
86
|
+
return config
|
emfuzzer/context.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Copyright (c) 2025 Warsaw University of Technology
|
|
2
|
+
# This file is licensed under the MIT License.
|
|
3
|
+
# See the LICENSE.txt file in the root of the repository for full details.
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
Module representing context of the experiment.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from abc import ABC, abstractmethod
|
|
10
|
+
from types import TracebackType
|
|
11
|
+
from typing import Self, cast
|
|
12
|
+
|
|
13
|
+
from .config import Config
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Worker(ABC):
|
|
17
|
+
@abstractmethod
|
|
18
|
+
def start(self) -> None: ...
|
|
19
|
+
|
|
20
|
+
@abstractmethod
|
|
21
|
+
def stop(self) -> None: ...
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Context:
|
|
25
|
+
|
|
26
|
+
def __init__(self, config: Config) -> None:
|
|
27
|
+
self._workers: dict[type[Worker], Worker] = {}
|
|
28
|
+
self._data: dict[str, object] = {}
|
|
29
|
+
self._config = config
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def config_root(self) -> Config:
|
|
33
|
+
return self._config
|
|
34
|
+
|
|
35
|
+
def worker[T: Worker](self, worker: type[T]) -> T:
|
|
36
|
+
if instance := self._workers.get(worker):
|
|
37
|
+
return cast(T, instance)
|
|
38
|
+
|
|
39
|
+
instance = worker()
|
|
40
|
+
instance.start()
|
|
41
|
+
|
|
42
|
+
self._workers[worker] = instance
|
|
43
|
+
|
|
44
|
+
return instance
|
|
45
|
+
|
|
46
|
+
def teardown(self) -> None:
|
|
47
|
+
for w in self._workers.values():
|
|
48
|
+
w.stop()
|
|
49
|
+
|
|
50
|
+
self._workers.clear()
|
|
51
|
+
|
|
52
|
+
def register_data(self, name: str, item: object) -> None:
|
|
53
|
+
if name in self._data:
|
|
54
|
+
raise RuntimeError(f"Data already registered: '{name}'")
|
|
55
|
+
|
|
56
|
+
self._data[name] = item
|
|
57
|
+
|
|
58
|
+
def data[T](self, data_type: type[T], name: str) -> T:
|
|
59
|
+
if item := self._data.get(name):
|
|
60
|
+
if isinstance(item, data_type):
|
|
61
|
+
return item
|
|
62
|
+
raise RuntimeError(f"Invalid data type for: '{name}'")
|
|
63
|
+
raise RuntimeError(f"Unknown data: '{name}'")
|
|
64
|
+
|
|
65
|
+
def __enter__(self) -> Self:
|
|
66
|
+
return self
|
|
67
|
+
|
|
68
|
+
def __exit__(
|
|
69
|
+
self,
|
|
70
|
+
exc_type: type[BaseException] | None,
|
|
71
|
+
exc_value: BaseException | None,
|
|
72
|
+
exc_traceback: TracebackType | None,
|
|
73
|
+
) -> None:
|
|
74
|
+
self.teardown()
|