dronecot 2.2.3__tar.gz → 2.2.5__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.
- {dronecot-2.2.3/dronecot.egg-info → dronecot-2.2.5}/PKG-INFO +1 -1
- {dronecot-2.2.3 → dronecot-2.2.5/dronecot.egg-info}/PKG-INFO +1 -1
- {dronecot-2.2.3 → dronecot-2.2.5}/dronecot.egg-info/SOURCES.txt +3 -1
- dronecot-2.2.5/src/dronecot/VERSION +1 -0
- {dronecot-2.2.3 → dronecot-2.2.5}/src/dronecot/__init__.py +2 -0
- {dronecot-2.2.3 → dronecot-2.2.5}/src/dronecot/constants.py +5 -0
- {dronecot-2.2.3 → dronecot-2.2.5}/src/dronecot/functions.py +28 -1
- {dronecot-2.2.3 → dronecot-2.2.5}/src/dronecot/wifi_parse.py +17 -36
- dronecot-2.2.5/tests/test_sensor_detail.py +54 -0
- dronecot-2.2.5/tests/test_wifi_parse.py +53 -0
- dronecot-2.2.3/src/dronecot/VERSION +0 -1
- {dronecot-2.2.3 → dronecot-2.2.5}/CHANGELOG.md +0 -0
- {dronecot-2.2.3 → dronecot-2.2.5}/LICENSE +0 -0
- {dronecot-2.2.3 → dronecot-2.2.5}/MANIFEST.in +0 -0
- {dronecot-2.2.3 → dronecot-2.2.5}/README.md +0 -0
- {dronecot-2.2.3 → dronecot-2.2.5}/dronecot.egg-info/dependency_links.txt +0 -0
- {dronecot-2.2.3 → dronecot-2.2.5}/dronecot.egg-info/entry_points.txt +0 -0
- {dronecot-2.2.3 → dronecot-2.2.5}/dronecot.egg-info/requires.txt +0 -0
- {dronecot-2.2.3 → dronecot-2.2.5}/dronecot.egg-info/top_level.txt +0 -0
- {dronecot-2.2.3 → dronecot-2.2.5}/setup.cfg +0 -0
- {dronecot-2.2.3 → dronecot-2.2.5}/setup.py +0 -0
- {dronecot-2.2.3 → dronecot-2.2.5}/src/dronecot/ble_capture.py +0 -0
- {dronecot-2.2.3 → dronecot-2.2.5}/src/dronecot/ble_parse.py +0 -0
- {dronecot-2.2.3 → dronecot-2.2.5}/src/dronecot/classes.py +0 -0
- {dronecot-2.2.3 → dronecot-2.2.5}/src/dronecot/commands.py +0 -0
- {dronecot-2.2.3 → dronecot-2.2.5}/src/dronecot/dji_exceptions.py +0 -0
- {dronecot-2.2.3 → dronecot-2.2.5}/src/dronecot/dji_functions.py +0 -0
- {dronecot-2.2.3 → dronecot-2.2.5}/src/dronecot/dji_text_parser.py +0 -0
- {dronecot-2.2.3 → dronecot-2.2.5}/src/dronecot/odid.py +0 -0
- {dronecot-2.2.3 → dronecot-2.2.5}/src/dronecot/open_drone_id.py +0 -0
- {dronecot-2.2.3 → dronecot-2.2.5}/src/dronecot/rid_normalize.py +0 -0
- {dronecot-2.2.3 → dronecot-2.2.5}/src/dronecot/udp_rid.py +0 -0
- {dronecot-2.2.3 → dronecot-2.2.5}/src/dronecot/wifi_capture.py +0 -0
- {dronecot-2.2.3 → dronecot-2.2.5}/tests/test_adsb_vehicle.py +0 -0
- {dronecot-2.2.3 → dronecot-2.2.5}/tests/test_functions.py +0 -0
- {dronecot-2.2.3 → dronecot-2.2.5}/tests/test_rid_normalize.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
2.2.5
|
|
@@ -41,6 +41,11 @@ DEFAULT_SENSOR_ID: str = f"dronecot_{hostname}"
|
|
|
41
41
|
|
|
42
42
|
DEFAULT_SENSOR_PAYLOAD_TYPE: str = "Unknown-Sensor-Payload-Type"
|
|
43
43
|
|
|
44
|
+
# Receiver hardware model (e.g. "Atheros AR9271") and detection method /
|
|
45
|
+
# capability (e.g. "Wi-Fi Open Drone ID"). Set via SENSOR_MODEL / SENSOR_TYPE.
|
|
46
|
+
DEFAULT_SENSOR_MODEL: str = "Unknown-Sensor"
|
|
47
|
+
DEFAULT_SENSOR_TYPE: str = "Remote ID"
|
|
48
|
+
|
|
44
49
|
DEFAULT_SENSOR_COT_TYPE: str = "a-f-G-E-S-E"
|
|
45
50
|
DEFAULT_OP_COT_TYPE: str = "a-n-G"
|
|
46
51
|
DEFAULT_UAS_COT_TYPE: str = "a-n-A-M-H-Q"
|
|
@@ -74,6 +74,19 @@ def _pacific_timestamp() -> str:
|
|
|
74
74
|
return datetime.now(tzinfo).strftime("%Y-%m-%d %H:%M:%S %Z")
|
|
75
75
|
|
|
76
76
|
|
|
77
|
+
def _rf_band(channel) -> str:
|
|
78
|
+
"""Map a Wi-Fi channel number to its RF band label (for SIGINT detail)."""
|
|
79
|
+
try:
|
|
80
|
+
ch = int(channel)
|
|
81
|
+
except (TypeError, ValueError):
|
|
82
|
+
return ""
|
|
83
|
+
if 1 <= ch <= 14:
|
|
84
|
+
return "2.4 GHz"
|
|
85
|
+
if 32 <= ch <= 196:
|
|
86
|
+
return "5 GHz"
|
|
87
|
+
return ""
|
|
88
|
+
|
|
89
|
+
|
|
77
90
|
def _dji_feed_uses_text(config, feed_url: str, parsed) -> bool:
|
|
78
91
|
"""Return True if the configured DJI feed uses AntSDR text CSV format."""
|
|
79
92
|
feed_format = str(config.get("FEED_FORMAT", "")).lower()
|
|
@@ -341,10 +354,19 @@ def rid_uas_to_cot_xml( # NOQA pylint: disable=too-many-locals,too-many-branche
|
|
|
341
354
|
sensor_type = str(src_data.get("type", dronecot.DEFAULT_SENSOR_PAYLOAD_TYPE))
|
|
342
355
|
rssi = str(src_data.get("RSSI"))
|
|
343
356
|
|
|
357
|
+
# Receiver / SIGINT detail: what hardware saw this, how, and on what RF band.
|
|
358
|
+
sensor_model = str(config.get("SENSOR_MODEL", dronecot.DEFAULT_SENSOR_MODEL))
|
|
359
|
+
sensor_method = str(config.get("SENSOR_TYPE", dronecot.DEFAULT_SENSOR_TYPE))
|
|
360
|
+
channel = src_data.get("channel")
|
|
361
|
+
band = _rf_band(channel)
|
|
362
|
+
|
|
344
363
|
cuas: ET.Element = ET.Element("__cuas")
|
|
345
364
|
cuas.set("sensor_id", sensor_id)
|
|
365
|
+
cuas.set("sensor_model", sensor_model)
|
|
366
|
+
cuas.set("sensor_method", sensor_method)
|
|
346
367
|
cuas.set("rssi", rssi)
|
|
347
|
-
cuas.set("channel", str(
|
|
368
|
+
cuas.set("channel", str(channel))
|
|
369
|
+
cuas.set("band", band)
|
|
348
370
|
cuas.set("timestamp", str(src_data.get("timestamp")))
|
|
349
371
|
cuas.set("mac_address", mac_address_text)
|
|
350
372
|
cuas.set("type", sensor_type)
|
|
@@ -374,6 +396,11 @@ def rid_uas_to_cot_xml( # NOQA pylint: disable=too-many-locals,too-many-branche
|
|
|
374
396
|
else:
|
|
375
397
|
callsign = f"{uasid[0:4]}...{uasid[-4:]}"
|
|
376
398
|
|
|
399
|
+
if rssi and rssi not in ("None", ""):
|
|
400
|
+
remarks_fields.append(f"RSSI: {rssi} dBm")
|
|
401
|
+
if channel is not None:
|
|
402
|
+
remarks_fields.append(f"Channel: {channel}{f' ({band})' if band else ''}")
|
|
403
|
+
remarks_fields.append(f"Receiver: {sensor_model} — {sensor_method}")
|
|
377
404
|
remarks_fields.append(f"Sensor: {sensor_id}")
|
|
378
405
|
|
|
379
406
|
contact: ET.Element = ET.Element("contact")
|
|
@@ -157,9 +157,16 @@ def extract_odid_from_dot11(frame_bytes: bytes) -> Optional[Tuple[bytes, Dict[st
|
|
|
157
157
|
|
|
158
158
|
|
|
159
159
|
def extract_odid_from_scapy_packet(packet) -> Optional[Tuple[bytes, Dict[str, Any]]]:
|
|
160
|
-
"""Extract ODID from a Scapy packet (Dot11 layer).
|
|
160
|
+
"""Extract ODID from a Scapy packet (Dot11 layer).
|
|
161
|
+
|
|
162
|
+
Delegates to the byte-level ``extract_odid_from_dot11`` parser on the
|
|
163
|
+
serialized Dot11 frame. This avoids a class of Scapy field-type bugs — e.g.
|
|
164
|
+
``Dot11EltVendorSpecific.oui`` is an int, so ``bytes(elt.oui)`` builds a huge
|
|
165
|
+
zero buffer instead of the 3 OUI bytes and the ASTM vendor element never
|
|
166
|
+
matches — and keeps Wi-Fi Beacon and NAN decoding on one proven code path.
|
|
167
|
+
"""
|
|
161
168
|
try:
|
|
162
|
-
from scapy.layers.dot11 import Dot11,
|
|
169
|
+
from scapy.layers.dot11 import Dot11, RadioTap # noqa
|
|
163
170
|
except ImportError:
|
|
164
171
|
return None
|
|
165
172
|
|
|
@@ -167,40 +174,14 @@ def extract_odid_from_scapy_packet(packet) -> Optional[Tuple[bytes, Dict[str, An
|
|
|
167
174
|
return None
|
|
168
175
|
|
|
169
176
|
dot11 = packet.getlayer(Dot11)
|
|
170
|
-
|
|
177
|
+
result = extract_odid_from_dot11(bytes(dot11))
|
|
178
|
+
if not result:
|
|
179
|
+
return None
|
|
171
180
|
|
|
172
|
-
|
|
173
|
-
|
|
181
|
+
pack, meta = result
|
|
182
|
+
# Enrich with Scapy-only signal info when present.
|
|
183
|
+
if packet.haslayer(RadioTap):
|
|
184
|
+
rssi = getattr(packet[RadioTap], "dBm_AntSignal", None)
|
|
174
185
|
if rssi is not None:
|
|
175
186
|
meta["RSSI"] = int(rssi)
|
|
176
|
-
|
|
177
|
-
subtype = dot11.subtype
|
|
178
|
-
|
|
179
|
-
if subtype == 8: # beacon
|
|
180
|
-
elt = packet.getlayer(Dot11EltVendorSpecific)
|
|
181
|
-
while elt:
|
|
182
|
-
if hasattr(elt, "oui") and hasattr(elt, "info"):
|
|
183
|
-
oui = bytes(elt.oui) if not isinstance(elt.oui, bytes) else elt.oui
|
|
184
|
-
info = bytes(elt.info) if elt.info else b""
|
|
185
|
-
ie_data = oui + info
|
|
186
|
-
pack = parse_beacon_vendor_ie(ie_data)
|
|
187
|
-
if pack:
|
|
188
|
-
meta["type"] = "WiFi beacon"
|
|
189
|
-
if dot11.IDinfo:
|
|
190
|
-
meta["channel"] = int(getattr(dot11.IDinfo, "channel", 0) or 0)
|
|
191
|
-
return pack, meta
|
|
192
|
-
elt = elt.payload.getlayer(Dot11EltVendorSpecific)
|
|
193
|
-
elif subtype == 13: # action
|
|
194
|
-
raw = bytes(packet[Dot11].payload) if dot11.payload else b""
|
|
195
|
-
if not raw and packet.haslayer("Raw"):
|
|
196
|
-
raw = bytes(packet["Raw"].load)
|
|
197
|
-
frame = bytes(dot11.build()) + raw if raw else bytes(packet.build())
|
|
198
|
-
# Rebuild full frame for NAN parser
|
|
199
|
-
full = bytes(packet.build())
|
|
200
|
-
result = extract_odid_from_dot11(full)
|
|
201
|
-
if result:
|
|
202
|
-
pack, nan_meta = result
|
|
203
|
-
meta.update(nan_meta)
|
|
204
|
-
return pack, meta
|
|
205
|
-
|
|
206
|
-
return None
|
|
187
|
+
return pack, meta
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Tests for the enriched sensor / SIGINT detail in the RID CoT payload."""
|
|
2
|
+
|
|
3
|
+
import xml.etree.ElementTree as ET
|
|
4
|
+
|
|
5
|
+
from dronecot.functions import _rf_band, rid_uas_to_cot_xml
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_rf_band():
|
|
9
|
+
assert _rf_band(6) == "2.4 GHz"
|
|
10
|
+
assert _rf_band("6") == "2.4 GHz"
|
|
11
|
+
assert _rf_band(44) == "5 GHz"
|
|
12
|
+
assert _rf_band(None) == ""
|
|
13
|
+
assert _rf_band("nope") == ""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _rid_data():
|
|
17
|
+
return {
|
|
18
|
+
"Latitude": 37.76,
|
|
19
|
+
"Longitude": -122.49,
|
|
20
|
+
"BasicID": "1787F04BM24010011195",
|
|
21
|
+
"SpeedHorizontal": 0,
|
|
22
|
+
"Direction": 0,
|
|
23
|
+
"AltitudeGeo": 10.0,
|
|
24
|
+
"data": {
|
|
25
|
+
"MAC address": "AA:BB:CC:DD:EE:FF",
|
|
26
|
+
"RSSI": -42,
|
|
27
|
+
"channel": 6,
|
|
28
|
+
"sensor_id": "wifi-rid",
|
|
29
|
+
},
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_rid_cot_carries_sensor_detail():
|
|
34
|
+
config = {
|
|
35
|
+
"SENSOR_MODEL": "Atheros AR9271",
|
|
36
|
+
"SENSOR_TYPE": "Wi-Fi Open Drone ID",
|
|
37
|
+
}
|
|
38
|
+
event = rid_uas_to_cot_xml(_rid_data(), config)
|
|
39
|
+
assert event is not None
|
|
40
|
+
xml = ET.tostring(event, encoding="unicode")
|
|
41
|
+
|
|
42
|
+
cuas = event.find(".//__cuas")
|
|
43
|
+
assert cuas is not None
|
|
44
|
+
assert cuas.get("sensor_model") == "Atheros AR9271"
|
|
45
|
+
assert cuas.get("sensor_method") == "Wi-Fi Open Drone ID"
|
|
46
|
+
assert cuas.get("band") == "2.4 GHz"
|
|
47
|
+
assert cuas.get("rssi") == "-42"
|
|
48
|
+
assert cuas.get("channel") == "6"
|
|
49
|
+
|
|
50
|
+
remarks = event.find(".//remarks")
|
|
51
|
+
assert remarks is not None and remarks.text
|
|
52
|
+
assert "RSSI: -42 dBm" in remarks.text
|
|
53
|
+
assert "2.4 GHz" in remarks.text
|
|
54
|
+
assert "Atheros AR9271" in remarks.text
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Tests for Wi-Fi Open Drone ID frame parsing.
|
|
2
|
+
|
|
3
|
+
Regression for the Scapy beacon path: ``Dot11EltVendorSpecific.oui`` is an int,
|
|
4
|
+
so the old ``bytes(elt.oui)`` built a huge zero buffer and the ASTM vendor
|
|
5
|
+
element never matched — ``extract_odid_from_scapy_packet`` decoded nothing even
|
|
6
|
+
though ``extract_odid_from_dot11`` decoded the same frame.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import struct
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
|
|
13
|
+
from dronecot import wifi_parse
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _astm_beacon_frame(pack_body: bytes) -> bytes:
|
|
17
|
+
"""Build a raw 802.11 beacon carrying an ASTM ODID vendor IE."""
|
|
18
|
+
fc = struct.pack("<H", 0x0080) # mgmt, subtype 8 (beacon)
|
|
19
|
+
dur = b"\x00\x00"
|
|
20
|
+
dst = b"\xff\xff\xff\xff\xff\xff"
|
|
21
|
+
src = b"\xaa\xbb\xcc\xdd\xee\xff"
|
|
22
|
+
bssid = src
|
|
23
|
+
seq = b"\x00\x00"
|
|
24
|
+
fixed = b"\x00" * 8 + b"\x64\x00" + b"\x00\x00" # timestamp + interval + caps
|
|
25
|
+
# Vendor IE: OUI FA:0B:BC, type 0x0D, msg_counter 0x01, then the pack body.
|
|
26
|
+
ie_payload = wifi_parse.ASTM_WIFI_OUI + bytes([0x0D, 0x01]) + pack_body
|
|
27
|
+
ie = bytes([0xDD, len(ie_payload)]) + ie_payload
|
|
28
|
+
return fc + dur + dst + src + bssid + seq + fixed + ie
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_extract_odid_from_dot11_beacon():
|
|
32
|
+
body = bytes(range(25))
|
|
33
|
+
frame = _astm_beacon_frame(body)
|
|
34
|
+
result = wifi_parse.extract_odid_from_dot11(frame)
|
|
35
|
+
assert result is not None
|
|
36
|
+
pack, meta = result
|
|
37
|
+
assert pack == body
|
|
38
|
+
assert meta.get("type") == "WiFi beacon"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_extract_odid_from_scapy_packet_matches_raw():
|
|
42
|
+
"""The Scapy path must decode the same beacon the byte parser does."""
|
|
43
|
+
scapy_dot11 = pytest.importorskip("scapy.layers.dot11")
|
|
44
|
+
body = bytes(range(25))
|
|
45
|
+
frame = _astm_beacon_frame(body)
|
|
46
|
+
pkt = scapy_dot11.Dot11(frame)
|
|
47
|
+
|
|
48
|
+
raw = wifi_parse.extract_odid_from_dot11(frame)
|
|
49
|
+
viascapy = wifi_parse.extract_odid_from_scapy_packet(pkt)
|
|
50
|
+
|
|
51
|
+
assert raw is not None
|
|
52
|
+
assert viascapy is not None, "scapy path failed on a frame the byte parser decoded"
|
|
53
|
+
assert viascapy[0] == raw[0] == body
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
2.2.3
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|