bdo-toolkit 1.0.1__tar.gz → 1.0.2__tar.gz
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.
- {bdo_toolkit-1.0.1/src/bdo_toolkit.egg-info → bdo_toolkit-1.0.2}/PKG-INFO +1 -1
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/src/bdo_toolkit/_capture_backend.py +121 -4
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/src/bdo_toolkit/_deposit_origin.py +579 -155
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/src/bdo_toolkit/_engine.py +9 -0
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/src/bdo_toolkit/_framing.py +28 -6
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/src/bdo_toolkit/_reassembly.py +20 -7
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/src/bdo_toolkit/_version.py +1 -1
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/src/bdo_toolkit/capture.py +6 -0
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/src/bdo_toolkit/character_state.py +16 -1
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2/src/bdo_toolkit.egg-info}/PKG-INFO +1 -1
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/src/bdo_toolkit.egg-info/SOURCES.txt +1 -0
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/tests/test_calibration_lifecycle.py +13 -1
- bdo_toolkit-1.0.2/tests/test_capture_backend.py +446 -0
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/tests/test_character_state.py +179 -3
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/tests/test_deposit_origin.py +834 -7
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/tests/test_inventory_snapshots.py +43 -1
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/tests/test_reassembly.py +133 -0
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/LICENSE +0 -0
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/README.md +0 -0
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/pyproject.toml +0 -0
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/setup.cfg +0 -0
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/src/bdo_toolkit/__init__.py +0 -0
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/src/bdo_toolkit/_async_sessions.py +0 -0
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/src/bdo_toolkit/_capture_options.py +0 -0
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/src/bdo_toolkit/_capture_runtime.py +0 -0
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/src/bdo_toolkit/_profile_runtime.py +0 -0
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/src/bdo_toolkit/_protocol.py +0 -0
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/src/bdo_toolkit/_specs.py +0 -0
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/src/bdo_toolkit/_storage_destination_validation.py +0 -0
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/src/bdo_toolkit/_storage_hydration.py +0 -0
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/src/bdo_toolkit/calibration.py +0 -0
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/src/bdo_toolkit/cli.py +0 -0
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/src/bdo_toolkit/diagnostics.py +0 -0
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/src/bdo_toolkit/events.py +0 -0
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/src/bdo_toolkit/filters.py +0 -0
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/src/bdo_toolkit/item_state.py +0 -0
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/src/bdo_toolkit/origin_learning.py +0 -0
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/src/bdo_toolkit/profiles.py +0 -0
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/src/bdo_toolkit/py.typed +0 -0
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/src/bdo_toolkit/remote_profiles.py +0 -0
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/src/bdo_toolkit/solare/__init__.py +0 -0
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/src/bdo_toolkit/solare/_constants.py +0 -0
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/src/bdo_toolkit/solare/_detail_learning.py +0 -0
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/src/bdo_toolkit/solare/_details.py +0 -0
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/src/bdo_toolkit/solare/_discovery.py +0 -0
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/src/bdo_toolkit/solare/_live_tracker.py +0 -0
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/src/bdo_toolkit/solare/_replay_capture.py +0 -0
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/src/bdo_toolkit/solare/_result.py +0 -0
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/src/bdo_toolkit/solare/_scanner.py +0 -0
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/src/bdo_toolkit/solare/_validation.py +0 -0
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/src/bdo_toolkit/solare/async_session.py +0 -0
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/src/bdo_toolkit/solare/models.py +0 -0
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/src/bdo_toolkit/solare/replay.py +0 -0
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/src/bdo_toolkit/solare/session.py +0 -0
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/src/bdo_toolkit/writers.py +0 -0
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/src/bdo_toolkit.egg-info/dependency_links.txt +0 -0
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/src/bdo_toolkit.egg-info/entry_points.txt +0 -0
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/src/bdo_toolkit.egg-info/requires.txt +0 -0
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/src/bdo_toolkit.egg-info/top_level.txt +0 -0
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/tests/test_async_sessions.py +0 -0
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/tests/test_calibration.py +0 -0
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/tests/test_calibration_conveniences.py +0 -0
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/tests/test_calibration_retention.py +0 -0
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/tests/test_capture_runtime.py +0 -0
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/tests/test_cli.py +0 -0
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/tests/test_decoder_diagnostics.py +0 -0
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/tests/test_direction.py +0 -0
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/tests/test_dynamic_storage_calibration_matrix.py +0 -0
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/tests/test_engine.py +0 -0
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/tests/test_examples_and_writers.py +0 -0
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/tests/test_explicit_profile.py +0 -0
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/tests/test_fixture_regressions.py +0 -0
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/tests/test_hardening.py +0 -0
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/tests/test_initial_load_storage.py +0 -0
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/tests/test_item_state.py +0 -0
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/tests/test_live_capture_session.py +0 -0
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/tests/test_origin_learning.py +0 -0
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/tests/test_remote_profiles.py +0 -0
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/tests/test_solare_api.py +0 -0
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/tests/test_solare_details.py +0 -0
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/tests/test_solare_learning.py +0 -0
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/tests/test_solare_learning_adversarial.py +0 -0
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/tests/test_solare_real_captures.py +0 -0
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/tests/test_solare_replay.py +0 -0
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/tests/test_solare_session.py +0 -0
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/tests/test_storage_destination_validation.py +0 -0
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/tests/test_storage_events.py +0 -0
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/tests/test_storage_hydration_tracker.py +0 -0
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/tests/test_storage_unknown_destination_calibration.py +0 -0
- {bdo_toolkit-1.0.1 → bdo_toolkit-1.0.2}/tests/test_toolkit_api.py +0 -0
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
from dataclasses import dataclass
|
|
6
6
|
from pathlib import Path
|
|
7
|
-
from typing import Iterable, Iterator, Optional, Protocol
|
|
7
|
+
from typing import Any, Iterable, Iterator, Optional, Protocol
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class SegmentConsumer(Protocol):
|
|
@@ -93,21 +93,138 @@ def validate_server_ports(ports: Iterable[int]) -> tuple[int, ...]:
|
|
|
93
93
|
return tuple(normalized)
|
|
94
94
|
|
|
95
95
|
|
|
96
|
+
def _required_header_integer(value: Any, field_name: str) -> int:
|
|
97
|
+
"""Return one decoded header field or reject an incomplete dissection."""
|
|
98
|
+
if value is None:
|
|
99
|
+
raise ValueError(f"IPv4/TCP {field_name} is unavailable")
|
|
100
|
+
try:
|
|
101
|
+
return int(value)
|
|
102
|
+
except (TypeError, ValueError, OverflowError) as exc:
|
|
103
|
+
raise ValueError(f"IPv4/TCP {field_name} is invalid: {value!r}") from exc
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _validate_unfragmented_ipv4_tcp(ip: Any) -> None:
|
|
107
|
+
"""Reject IP states that cannot be handed to TCP reassembly safely."""
|
|
108
|
+
version = _required_header_integer(getattr(ip, "version", None), "version")
|
|
109
|
+
if version != 4:
|
|
110
|
+
raise ValueError(f"expected IPv4 version 4, got {version}")
|
|
111
|
+
|
|
112
|
+
fragment_offset = _required_header_integer(
|
|
113
|
+
getattr(ip, "frag", None),
|
|
114
|
+
"fragment offset",
|
|
115
|
+
)
|
|
116
|
+
flags = _required_header_integer(getattr(ip, "flags", None), "flags")
|
|
117
|
+
if fragment_offset != 0 or flags & 0x01:
|
|
118
|
+
raise ValueError(
|
|
119
|
+
"fragmented IPv4/TCP packets are unsupported; "
|
|
120
|
+
"TCP reassembly requires a complete IP datagram"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _captured_ipv4_length(ip: Any) -> int:
|
|
125
|
+
"""Return captured bytes without reserializing ordinary Scapy packets."""
|
|
126
|
+
original = getattr(ip, "original", None)
|
|
127
|
+
if isinstance(original, (bytes, bytearray, memoryview)) and original:
|
|
128
|
+
return len(original)
|
|
129
|
+
return len(bytes(ip))
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _extract_ipv4_tcp_payload(ip: Any, tcp: Any) -> bytes:
|
|
133
|
+
"""Return only application bytes declared by complete IPv4/TCP headers.
|
|
134
|
+
|
|
135
|
+
Scapy exposes link-layer padding through ``tcp.payload`` even though those
|
|
136
|
+
bytes lie beyond the IPv4 total length. The wire header lengths are the
|
|
137
|
+
authority at this boundary so padding can never advance TCP sequence state.
|
|
138
|
+
"""
|
|
139
|
+
total_length = _required_header_integer(
|
|
140
|
+
getattr(ip, "len", None),
|
|
141
|
+
"total length",
|
|
142
|
+
)
|
|
143
|
+
ip_header_words = _required_header_integer(
|
|
144
|
+
getattr(ip, "ihl", None),
|
|
145
|
+
"header length",
|
|
146
|
+
)
|
|
147
|
+
tcp_header_words = _required_header_integer(
|
|
148
|
+
getattr(tcp, "dataofs", None),
|
|
149
|
+
"TCP header length",
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
if not 5 <= ip_header_words <= 15:
|
|
153
|
+
raise ValueError(
|
|
154
|
+
f"invalid IPv4 header length: {ip_header_words} 32-bit words"
|
|
155
|
+
)
|
|
156
|
+
if not 5 <= tcp_header_words <= 15:
|
|
157
|
+
raise ValueError(
|
|
158
|
+
f"invalid TCP header length: {tcp_header_words} 32-bit words"
|
|
159
|
+
)
|
|
160
|
+
if not 0 <= total_length <= 0xFFFF:
|
|
161
|
+
raise ValueError(f"invalid IPv4 total length: {total_length}")
|
|
162
|
+
|
|
163
|
+
captured_length = _captured_ipv4_length(ip)
|
|
164
|
+
if captured_length < total_length:
|
|
165
|
+
raise ValueError(
|
|
166
|
+
"truncated IPv4 packet: "
|
|
167
|
+
f"header declares {total_length} bytes, capture has {captured_length}"
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
header_length = (ip_header_words + tcp_header_words) * 4
|
|
171
|
+
if total_length < header_length:
|
|
172
|
+
raise ValueError(
|
|
173
|
+
"invalid IPv4/TCP header lengths: "
|
|
174
|
+
f"total length {total_length} is smaller than {header_length}"
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
declared_length = total_length - header_length
|
|
178
|
+
available = bytes(getattr(tcp, "payload", b""))
|
|
179
|
+
if len(available) < declared_length:
|
|
180
|
+
raise ValueError(
|
|
181
|
+
"truncated IPv4/TCP payload: "
|
|
182
|
+
f"header declares {declared_length} bytes, capture has {len(available)}"
|
|
183
|
+
)
|
|
184
|
+
return available[:declared_length]
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _consumer_server_ports(engine: SegmentConsumer) -> Optional[frozenset[int]]:
|
|
188
|
+
"""Read the validated port set exposed by each built-in consumer."""
|
|
189
|
+
ports = getattr(engine, "server_ports", None)
|
|
190
|
+
if ports is None:
|
|
191
|
+
# Solare's internal collector predates the shared ``server_ports`` name.
|
|
192
|
+
ports = getattr(engine, "ports", None)
|
|
193
|
+
if ports is None:
|
|
194
|
+
return None
|
|
195
|
+
return frozenset(ports)
|
|
196
|
+
|
|
197
|
+
|
|
96
198
|
def make_packet_handler(engine: SegmentConsumer):
|
|
97
199
|
IP, TCP, _, _, _ = import_scapy()
|
|
200
|
+
server_ports = _consumer_server_ports(engine)
|
|
98
201
|
|
|
99
202
|
def handle(packet) -> None:
|
|
100
|
-
if IP not in packet
|
|
203
|
+
if IP not in packet:
|
|
101
204
|
return
|
|
102
205
|
|
|
103
206
|
ip = packet[IP]
|
|
207
|
+
protocol = _required_header_integer(getattr(ip, "proto", None), "protocol")
|
|
208
|
+
if protocol != 6:
|
|
209
|
+
return
|
|
210
|
+
|
|
211
|
+
if TCP not in packet:
|
|
212
|
+
# Non-initial fragments and severely truncated headers have no
|
|
213
|
+
# source port, so they cannot be attributed to a selected flow.
|
|
214
|
+
return
|
|
215
|
+
|
|
104
216
|
tcp = packet[TCP]
|
|
105
|
-
|
|
217
|
+
source_port = int(tcp.sport)
|
|
218
|
+
if server_ports is not None and source_port not in server_ports:
|
|
219
|
+
return
|
|
220
|
+
|
|
221
|
+
_validate_unfragmented_ipv4_tcp(ip)
|
|
222
|
+
payload = _extract_ipv4_tcp_payload(ip, tcp)
|
|
106
223
|
flags = int(tcp.flags)
|
|
107
224
|
|
|
108
225
|
engine.process_tcp_segment(
|
|
109
226
|
source_ip=str(ip.src),
|
|
110
|
-
source_port=
|
|
227
|
+
source_port=source_port,
|
|
111
228
|
destination_ip=str(ip.dst),
|
|
112
229
|
destination_port=int(tcp.dport),
|
|
113
230
|
sequence=int(tcp.seq),
|