iotsploit-protocols 0.0.9__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.
@@ -0,0 +1,252 @@
1
+ """What a target says about one CAN frame, frozen and free of storage detail.
2
+
3
+ A target holds CAN frames in two places for historical reasons: ARXML frames
4
+ and transmitter-less DBC frames sit under ``bus.properties.messages``, while
5
+ transmitter-attributed DBC frames sit under ``component.facets.can.messages``.
6
+ Those are different JSON shapes describing the same wire, and every consumer
7
+ that reads them directly grows its own opinion about which fields exist.
8
+
9
+ These types are the single shape everything downstream works in. The catalogue
10
+ converts both storage forms into them once; the codec, the composer plugin, the
11
+ live capture, and the UI all read them and never touch the raw target again.
12
+
13
+ Nothing here validates a bit layout. That is the codec's job, because the
14
+ authority on whether a signal fits is ``cantools``, and duplicating its rules
15
+ here would produce a second answer to the same question.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ from dataclasses import dataclass, field
21
+ from typing import Any, Dict, Mapping, Optional, Sequence, Tuple
22
+
23
+ #: Frame ids wider than these cannot be put on a wire. Standard CAN carries an
24
+ #: 11-bit identifier and extended CAN a 29-bit one.
25
+ MAX_STANDARD_FRAME_ID = 0x7FF
26
+ MAX_EXTENDED_FRAME_ID = 0x1FFFFFFF
27
+
28
+
29
+ @dataclass(frozen=True)
30
+ class SignalDefinition:
31
+ """One field packed into a frame's payload.
32
+
33
+ Carries everything needed to rebuild a ``cantools`` signal and nothing that
34
+ only describes where it was stored. ``factor`` and ``offset`` convert raw
35
+ to physical: ``physical = raw * factor + offset``.
36
+ """
37
+
38
+ name: str
39
+ start_bit: int
40
+ length: int
41
+ #: ``"little"`` (Intel, DBC ``@1``) or ``"big"`` (Motorola, DBC ``@0``).
42
+ byte_order: str = "little"
43
+ signed: bool = False
44
+ factor: float = 1.0
45
+ offset: float = 0.0
46
+ minimum: Optional[float] = None
47
+ maximum: Optional[float] = None
48
+ unit: str = ""
49
+ #: ``"M"`` for the switch, ``"m3"`` or ``"m3,m4"`` for a branch signal.
50
+ multiplexer: Optional[str] = None
51
+ #: Which switch selects this branch signal. Only load-bearing when a frame
52
+ #: has more than one multiplexer, but stored always so it never has to be
53
+ #: inferred from position.
54
+ multiplexer_signal: Optional[str] = None
55
+ choices: Optional[Mapping[int, str]] = None
56
+ is_float: bool = False
57
+
58
+ @property
59
+ def is_multiplexer(self) -> bool:
60
+ """Whether this signal selects which branch signals are present."""
61
+ return self.multiplexer == "M"
62
+
63
+ @property
64
+ def multiplexer_ids(self) -> Tuple[int, ...]:
65
+ """The switch values this signal is present for.
66
+
67
+ Empty for ordinary signals and for the switch itself. ARXML joins
68
+ several with commas because one signal can belong to more than one
69
+ branch; DBC only ever writes a single value.
70
+ """
71
+ if not self.multiplexer or self.multiplexer == "M":
72
+ return ()
73
+ ids = []
74
+ for token in self.multiplexer.split(","):
75
+ token = token.strip()
76
+ if token.startswith("m") and token[1:].lstrip("-").isdigit():
77
+ ids.append(int(token[1:]))
78
+ return tuple(ids)
79
+
80
+
81
+ @dataclass(frozen=True)
82
+ class FrameDefinition:
83
+ """One frame on one bus, with where it came from attached.
84
+
85
+ ``(bus_id, frame_id, is_extended)`` is the identity. A name is not: the
86
+ same name legitimately appears on two buses, and 0x123 standard and 0x123
87
+ extended are different frames sharing a wire.
88
+
89
+ ``unsupported_reason`` is set rather than raised so that a frame the
90
+ composer cannot encode is still listable. The UI shows it as a disabled row
91
+ explaining itself, which is more useful than omitting it and leaving the
92
+ operator to wonder where it went.
93
+ """
94
+
95
+ bus_id: str
96
+ frame_id: int
97
+ is_extended: bool
98
+ name: str
99
+ dlc: int
100
+ signals: Tuple[SignalDefinition, ...] = ()
101
+ is_fd: bool = False
102
+ senders: Tuple[str, ...] = ()
103
+ cycle_time_ms: Optional[int] = None
104
+ #: ``"bus"`` or ``"component"`` -- which storage location this came from.
105
+ owner_kind: str = "bus"
106
+ component_id: Optional[str] = None
107
+ component_name: Optional[str] = None
108
+ #: Index in the list it was read from, so a diagnostic can point at the row.
109
+ source_index: int = 0
110
+ #: AUTOSAR container payloads, kept only to detect and refuse one.
111
+ contained_messages: Tuple[Mapping[str, Any], ...] = ()
112
+ unsupported_reason: Optional[str] = None
113
+
114
+ @property
115
+ def is_supported(self) -> bool:
116
+ return self.unsupported_reason is None
117
+
118
+ @property
119
+ def frame_id_hex(self) -> str:
120
+ return f"0x{self.frame_id:X}"
121
+
122
+ @property
123
+ def multiplexer_signal_name(self) -> Optional[str]:
124
+ """The name of this frame's switch signal, if it has one."""
125
+ for signal in self.signals:
126
+ if signal.is_multiplexer:
127
+ return signal.name
128
+ return None
129
+
130
+ def signal(self, name: str) -> Optional[SignalDefinition]:
131
+ for signal in self.signals:
132
+ if signal.name == name:
133
+ return signal
134
+ return None
135
+
136
+ def encoding_key(self) -> Tuple[Any, ...]:
137
+ """Everything that changes the bytes on the wire, and nothing else.
138
+
139
+ Two rows describing one frame are duplicates when this matches. Where
140
+ they came from, who sends them, and how often deliberately do not
141
+ appear: two components declaring the same frame identically is
142
+ ordinary, and treating that as a conflict would make half the DBCs in
143
+ the world unusable.
144
+ """
145
+ return (
146
+ self.frame_id,
147
+ self.is_extended,
148
+ self.is_fd,
149
+ self.name,
150
+ self.dlc,
151
+ tuple(
152
+ (
153
+ s.name,
154
+ s.start_bit,
155
+ s.length,
156
+ s.byte_order,
157
+ s.signed,
158
+ s.factor,
159
+ s.offset,
160
+ s.unit,
161
+ s.multiplexer,
162
+ s.multiplexer_signal,
163
+ s.is_float,
164
+ tuple(sorted((s.choices or {}).items())),
165
+ )
166
+ for s in self.signals
167
+ ),
168
+ bool(self.contained_messages),
169
+ )
170
+
171
+
172
+ @dataclass(frozen=True)
173
+ class BusDefinition:
174
+ """A CAN bus and the frames resolved onto it."""
175
+
176
+ bus_id: str
177
+ name: str
178
+ frames: Tuple[FrameDefinition, ...] = ()
179
+ #: Identities that resolved to more than one incompatible definition.
180
+ #: Listed separately because they are findings about the target, not
181
+ #: frames anyone can use.
182
+ conflicts: Tuple[str, ...] = ()
183
+
184
+ def frame(self, frame_id: int, is_extended: bool) -> Optional[FrameDefinition]:
185
+ for candidate in self.frames:
186
+ if candidate.frame_id == frame_id and candidate.is_extended == is_extended:
187
+ return candidate
188
+ return None
189
+
190
+
191
+ @dataclass(frozen=True)
192
+ class EncodedFrame:
193
+ """The bytes a frame encodes to, with the flags needed to put them on a wire."""
194
+
195
+ frame_id: int
196
+ is_extended: bool
197
+ is_fd: bool
198
+ dlc: int
199
+ data: bytes
200
+ name: str
201
+ #: Values after normalization: choice labels resolved, numeric text parsed,
202
+ #: nothing echoed back as the operator typed it.
203
+ signals: Dict[str, Any] = field(default_factory=dict)
204
+
205
+ @property
206
+ def data_hex(self) -> str:
207
+ return self.data.hex().upper()
208
+
209
+
210
+ @dataclass(frozen=True)
211
+ class DecodedFrame:
212
+ """The result of reading bytes against a definition.
213
+
214
+ Never raised, always returned, because the bytes come off a wire and a
215
+ capture that dies on one malformed frame is not a capture. ``ok`` says
216
+ which half of this object is meaningful.
217
+ """
218
+
219
+ ok: bool
220
+ name: str
221
+ signals: Dict[str, Any] = field(default_factory=dict)
222
+ #: Raw integers behind any named choice, so a code missing from the value
223
+ #: table is reported as the number it was rather than lost.
224
+ raw_values: Dict[str, int] = field(default_factory=dict)
225
+ reason: Optional[str] = None
226
+
227
+ @classmethod
228
+ def failed(cls, name: str, reason: str) -> "DecodedFrame":
229
+ return cls(ok=False, name=name, reason=reason)
230
+
231
+
232
+ def canonical_frame_id(frame_id: int, is_extended: bool = False) -> str:
233
+ """The string form of a frame id, matching the CAN facet's own function.
234
+
235
+ Duplicated from ``iotsploit_django.tools.can_facet`` on purpose: this
236
+ package must not import Django, and an observation's ``subject_id`` has to
237
+ join across both. Uppercase hex, no ``0x``, zero-padded to the width of the
238
+ id space -- three digits standard, eight extended -- so a standard 0x123
239
+ and an extended 0x123 stay distinct keys.
240
+ """
241
+ return f"{frame_id:0{8 if is_extended else 3}X}"
242
+
243
+
244
+ def frame_id_is_valid(frame_id: int, is_extended: bool) -> bool:
245
+ """Whether a frame id fits the identifier width it claims."""
246
+ if not isinstance(frame_id, int) or isinstance(frame_id, bool) or frame_id < 0:
247
+ return False
248
+ return frame_id <= (MAX_EXTENDED_FRAME_ID if is_extended else MAX_STANDARD_FRAME_ID)
249
+
250
+
251
+ def signals_by_name(signals: Sequence[SignalDefinition]) -> Dict[str, SignalDefinition]:
252
+ return {signal.name: signal for signal in signals}
@@ -0,0 +1,154 @@
1
+ """Telling a bus fault apart from a frame, before anything reads an address.
2
+
3
+ A SocketCAN socket delivers bus faults as *error frames*, and python-can turns
4
+ them on by default (``CAN_RAW_ERR_FILTER``). They are not messages. The
5
+ ``CAN_ERR_FLAG`` lives in the CAN ID, and python-can masks it off before it
6
+ exposes ``arbitration_id``, so what is left there is an error *class*, not an
7
+ address: ``CAN_ERR_CRTL`` arrives looking exactly like a frame with id ``0x004``
8
+ whose ``data[1]`` happens to be the controller status.
9
+
10
+ Anything that keys on ``(arbitration_id, is_extended)`` without testing
11
+ ``is_error_frame`` first therefore invents a frame ``0x004`` no ECU ever sent
12
+ and counts bus faults as traffic. For a capture that writes observations the
13
+ consequence is worse than a wrong number on a screen: it records that phantom
14
+ frame as a documented-versus-observed finding against the target.
15
+
16
+ Why this exists twice
17
+ ---------------------
18
+ ``iotsploit_drivers.socketcan.can_errors`` holds the same constants, and the
19
+ duplication is deliberate rather than an oversight. ``iotsploit-drivers``
20
+ depends on ``iotsploit-core`` alone, and the dependency direction both CAN plans
21
+ fix is ``exploits -> protocols -> core``; importing the driver package from here
22
+ would add an edge neither plan sanctions, to reach nine integers out of
23
+ ``linux/can/error.h`` that have not changed since 2007. The precedent is
24
+ ``canonical_frame_id``, duplicated for the same reason.
25
+
26
+ The two are not redundant in what they produce. The driver formats a
27
+ ``candump -e`` style description for a live stream; a capture needs a per-class
28
+ tally it can total over a window. They share the vocabulary, not the output.
29
+ """
30
+
31
+ from __future__ import annotations
32
+
33
+ from dataclasses import dataclass
34
+ from typing import Any, Dict, List, Mapping, Tuple
35
+
36
+ # Error class, carried in the masked arbitration id. linux/can/error.h.
37
+ CAN_ERR_TX_TIMEOUT = 0x001
38
+ CAN_ERR_LOSTARB = 0x002
39
+ CAN_ERR_CRTL = 0x004
40
+ CAN_ERR_PROT = 0x008
41
+ CAN_ERR_TRX = 0x010
42
+ CAN_ERR_ACK = 0x020
43
+ CAN_ERR_BUSOFF = 0x040
44
+ CAN_ERR_BUSERROR = 0x080
45
+ CAN_ERR_RESTARTED = 0x100
46
+
47
+ ERROR_CLASSES: Dict[int, str] = {
48
+ CAN_ERR_TX_TIMEOUT: "tx-timeout",
49
+ CAN_ERR_LOSTARB: "lost-arbitration",
50
+ CAN_ERR_CRTL: "controller-problem",
51
+ CAN_ERR_PROT: "protocol-violation",
52
+ CAN_ERR_TRX: "transceiver-status",
53
+ CAN_ERR_ACK: "no-acknowledgement-on-tx",
54
+ CAN_ERR_BUSOFF: "bus-off",
55
+ CAN_ERR_BUSERROR: "bus-error",
56
+ CAN_ERR_RESTARTED: "controller-restarted",
57
+ }
58
+
59
+ #: Controller status, ``data[1]``. These are the ones worth surfacing to an
60
+ #: operator: a bus sitting at error-passive is saying the bitrate is wrong, the
61
+ #: FD configuration is wrong, or the wiring is wrong -- which is the actual
62
+ #: explanation for a capture that "sees nothing".
63
+ CONTROLLER_STATUS: Dict[int, str] = {
64
+ 0x01: "rx-overflow",
65
+ 0x02: "tx-overflow",
66
+ 0x04: "rx-error-warning",
67
+ 0x08: "tx-error-warning",
68
+ 0x10: "rx-error-passive",
69
+ 0x20: "tx-error-passive",
70
+ 0x40: "back-to-error-active",
71
+ }
72
+
73
+ #: Statuses that mean the link is degraded rather than merely noisy. A capture
74
+ #: whose data-frame count is low and whose faults are these must say so instead
75
+ #: of reporting an empty bus.
76
+ DEGRADED_STATUSES = frozenset(
77
+ {"rx-error-passive", "tx-error-passive", "rx-error-warning", "tx-error-warning"}
78
+ )
79
+
80
+
81
+ @dataclass(frozen=True)
82
+ class ErrorFrame:
83
+ """One classified fault, reduced to what a tally needs."""
84
+
85
+ classes: Tuple[str, ...]
86
+ controller_status: Tuple[str, ...] = ()
87
+
88
+ @property
89
+ def labels(self) -> Tuple[str, ...]:
90
+ """Every name this fault should be counted under."""
91
+ return self.classes + self.controller_status
92
+
93
+ @property
94
+ def is_degraded(self) -> bool:
95
+ return "bus-off" in self.classes or any(
96
+ status in DEGRADED_STATUSES for status in self.controller_status
97
+ )
98
+
99
+
100
+ def _flag_names(mapping: Mapping[int, str], value: int) -> List[str]:
101
+ return [name for bit, name in sorted(mapping.items()) if value & bit]
102
+
103
+
104
+ def classify_error_frame(arbitration_id: int, data: Any = None) -> ErrorFrame:
105
+ """Read an error frame's class, and the controller status when it has one.
106
+
107
+ ``arbitration_id`` is what python-can exposes after masking off
108
+ ``CAN_ERR_FLAG``: a bitmask of error classes, never an address.
109
+
110
+ Never raises. A live capture is a long-running loop, so a message-shaped
111
+ object carrying something other than an ``int`` and ``bytes`` degrades to
112
+ ``unknown-error-class`` rather than ending the capture.
113
+ """
114
+ try:
115
+ flags = int(arbitration_id or 0)
116
+ except (TypeError, ValueError):
117
+ flags = 0
118
+
119
+ classes = tuple(_flag_names(ERROR_CLASSES, flags))
120
+ if not classes:
121
+ classes = ("unknown-error-class",)
122
+
123
+ status: Tuple[str, ...] = ()
124
+ if flags & CAN_ERR_CRTL:
125
+ # An int is excluded rather than coerced: bytes(n) would allocate n
126
+ # zero bytes, which is both a wrong reading and a way to exhaust a
127
+ # long-running capture's memory from one malformed message.
128
+ try:
129
+ payload = b"" if isinstance(data, int) else bytes(data or b"")
130
+ except (TypeError, ValueError):
131
+ payload = b""
132
+ if len(payload) > 1:
133
+ status = tuple(_flag_names(CONTROLLER_STATUS, payload[1]))
134
+
135
+ return ErrorFrame(classes=classes, controller_status=status)
136
+
137
+
138
+ def is_error_frame(message: Any) -> bool:
139
+ """Whether this is a fault rather than traffic.
140
+
141
+ Read before identity, always. A message object that does not carry the
142
+ attribute is treated as traffic, which is the safe default for a scripted
143
+ frame source in a test.
144
+ """
145
+ return bool(getattr(message, "is_error_frame", False))
146
+
147
+
148
+ def is_remote_frame(message: Any) -> bool:
149
+ """Whether this is a remote request rather than a data frame.
150
+
151
+ Its own category: a remote frame carries no payload, and counting it as a
152
+ zero-length data frame would report a frame whose signals are all zero.
153
+ """
154
+ return bool(getattr(message, "is_remote_frame", False))
@@ -0,0 +1,47 @@
1
+ """Errors the CAN catalogue and codec raise.
2
+
3
+ Two types, split the way :mod:`iotsploit_protocols.errors` splits its three:
4
+ by what the caller can do about it.
5
+
6
+ ``CanDefinitionError`` the target is wrong -- fix the ARXML, DBC, or facet.
7
+ ``CanValueError`` the operator's input is wrong -- fix the value.
8
+
9
+ The difference is not cosmetic. A definition error means no value would have
10
+ worked and the form should say so once, at the top; a value error belongs
11
+ against the signal row that caused it, which is why it carries a field map.
12
+
13
+ Transport failures are deliberately absent. The house rule in
14
+ :mod:`iotsploit_protocols.errors` is that a connection failure stays
15
+ ``OSError`` rather than being re-spelled. ``python-can`` does not follow it: a
16
+ SocketCAN interface that is down surfaces as ``can.CanOperationError``, which
17
+ is not an ``OSError``, so :mod:`iotsploit_protocols.canbus.socketcan` wraps it
18
+ in ``CanTransportError`` rather than let a caller's ``except OSError`` miss it.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ from typing import Dict, Optional
24
+
25
+ from iotsploit_protocols.errors import ProtocolError
26
+
27
+
28
+ class CanDefinitionError(ProtocolError):
29
+ """The target cannot describe the requested frame.
30
+
31
+ Covers absent, ambiguous, conflicting, and unsupported definitions. The
32
+ caller cannot fix any of them by changing a signal value, which is what
33
+ separates this from :class:`CanValueError`.
34
+ """
35
+
36
+
37
+ class CanValueError(ProtocolError):
38
+ """A supplied signal value is missing, unknown, or out of range.
39
+
40
+ ``field_errors`` maps a dotted path (``signals.VehicleSpeed``) to a message
41
+ about that one field, so an editor can put the text next to the input that
42
+ caused it instead of showing one opaque failure for the whole frame.
43
+ """
44
+
45
+ def __init__(self, message: str, field_errors: Optional[Dict[str, str]] = None) -> None:
46
+ self.field_errors: Dict[str, str] = dict(field_errors or {})
47
+ super().__init__(message)