oscura 0.5.1__py3-none-any.whl → 0.7.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.
Files changed (497) hide show
  1. oscura/__init__.py +169 -167
  2. oscura/analyzers/__init__.py +3 -0
  3. oscura/analyzers/classification.py +659 -0
  4. oscura/analyzers/digital/edges.py +325 -65
  5. oscura/analyzers/digital/quality.py +293 -166
  6. oscura/analyzers/digital/timing.py +260 -115
  7. oscura/analyzers/digital/timing_numba.py +334 -0
  8. oscura/analyzers/entropy.py +605 -0
  9. oscura/analyzers/eye/diagram.py +176 -109
  10. oscura/analyzers/eye/metrics.py +5 -5
  11. oscura/analyzers/jitter/__init__.py +6 -4
  12. oscura/analyzers/jitter/ber.py +52 -52
  13. oscura/analyzers/jitter/classification.py +156 -0
  14. oscura/analyzers/jitter/decomposition.py +163 -113
  15. oscura/analyzers/jitter/spectrum.py +80 -64
  16. oscura/analyzers/ml/__init__.py +39 -0
  17. oscura/analyzers/ml/features.py +600 -0
  18. oscura/analyzers/ml/signal_classifier.py +604 -0
  19. oscura/analyzers/packet/daq.py +246 -158
  20. oscura/analyzers/packet/parser.py +12 -1
  21. oscura/analyzers/packet/payload.py +50 -2110
  22. oscura/analyzers/packet/payload_analysis.py +361 -181
  23. oscura/analyzers/packet/payload_patterns.py +133 -70
  24. oscura/analyzers/packet/stream.py +84 -23
  25. oscura/analyzers/patterns/__init__.py +26 -5
  26. oscura/analyzers/patterns/anomaly_detection.py +908 -0
  27. oscura/analyzers/patterns/clustering.py +169 -108
  28. oscura/analyzers/patterns/clustering_optimized.py +227 -0
  29. oscura/analyzers/patterns/discovery.py +1 -1
  30. oscura/analyzers/patterns/matching.py +581 -197
  31. oscura/analyzers/patterns/pattern_mining.py +778 -0
  32. oscura/analyzers/patterns/periodic.py +121 -38
  33. oscura/analyzers/patterns/sequences.py +175 -78
  34. oscura/analyzers/power/conduction.py +1 -1
  35. oscura/analyzers/power/soa.py +6 -6
  36. oscura/analyzers/power/switching.py +250 -110
  37. oscura/analyzers/protocol/__init__.py +17 -1
  38. oscura/analyzers/protocols/base.py +6 -6
  39. oscura/analyzers/protocols/ble/__init__.py +38 -0
  40. oscura/analyzers/protocols/ble/analyzer.py +809 -0
  41. oscura/analyzers/protocols/ble/uuids.py +288 -0
  42. oscura/analyzers/protocols/can.py +257 -127
  43. oscura/analyzers/protocols/can_fd.py +107 -80
  44. oscura/analyzers/protocols/flexray.py +139 -80
  45. oscura/analyzers/protocols/hdlc.py +93 -58
  46. oscura/analyzers/protocols/i2c.py +247 -106
  47. oscura/analyzers/protocols/i2s.py +138 -86
  48. oscura/analyzers/protocols/industrial/__init__.py +40 -0
  49. oscura/analyzers/protocols/industrial/bacnet/__init__.py +33 -0
  50. oscura/analyzers/protocols/industrial/bacnet/analyzer.py +708 -0
  51. oscura/analyzers/protocols/industrial/bacnet/encoding.py +412 -0
  52. oscura/analyzers/protocols/industrial/bacnet/services.py +622 -0
  53. oscura/analyzers/protocols/industrial/ethercat/__init__.py +30 -0
  54. oscura/analyzers/protocols/industrial/ethercat/analyzer.py +474 -0
  55. oscura/analyzers/protocols/industrial/ethercat/mailbox.py +339 -0
  56. oscura/analyzers/protocols/industrial/ethercat/topology.py +166 -0
  57. oscura/analyzers/protocols/industrial/modbus/__init__.py +31 -0
  58. oscura/analyzers/protocols/industrial/modbus/analyzer.py +525 -0
  59. oscura/analyzers/protocols/industrial/modbus/crc.py +79 -0
  60. oscura/analyzers/protocols/industrial/modbus/functions.py +436 -0
  61. oscura/analyzers/protocols/industrial/opcua/__init__.py +21 -0
  62. oscura/analyzers/protocols/industrial/opcua/analyzer.py +552 -0
  63. oscura/analyzers/protocols/industrial/opcua/datatypes.py +446 -0
  64. oscura/analyzers/protocols/industrial/opcua/services.py +264 -0
  65. oscura/analyzers/protocols/industrial/profinet/__init__.py +23 -0
  66. oscura/analyzers/protocols/industrial/profinet/analyzer.py +441 -0
  67. oscura/analyzers/protocols/industrial/profinet/dcp.py +263 -0
  68. oscura/analyzers/protocols/industrial/profinet/ptcp.py +200 -0
  69. oscura/analyzers/protocols/jtag.py +180 -98
  70. oscura/analyzers/protocols/lin.py +219 -114
  71. oscura/analyzers/protocols/manchester.py +4 -4
  72. oscura/analyzers/protocols/onewire.py +253 -149
  73. oscura/analyzers/protocols/parallel_bus/__init__.py +20 -0
  74. oscura/analyzers/protocols/parallel_bus/centronics.py +92 -0
  75. oscura/analyzers/protocols/parallel_bus/gpib.py +137 -0
  76. oscura/analyzers/protocols/spi.py +192 -95
  77. oscura/analyzers/protocols/swd.py +321 -167
  78. oscura/analyzers/protocols/uart.py +267 -125
  79. oscura/analyzers/protocols/usb.py +235 -131
  80. oscura/analyzers/side_channel/power.py +17 -12
  81. oscura/analyzers/signal/__init__.py +15 -0
  82. oscura/analyzers/signal/timing_analysis.py +1086 -0
  83. oscura/analyzers/signal_integrity/__init__.py +4 -1
  84. oscura/analyzers/signal_integrity/sparams.py +2 -19
  85. oscura/analyzers/spectral/chunked.py +129 -60
  86. oscura/analyzers/spectral/chunked_fft.py +300 -94
  87. oscura/analyzers/spectral/chunked_wavelet.py +100 -80
  88. oscura/analyzers/statistical/checksum.py +376 -217
  89. oscura/analyzers/statistical/classification.py +229 -107
  90. oscura/analyzers/statistical/entropy.py +78 -53
  91. oscura/analyzers/statistics/correlation.py +407 -211
  92. oscura/analyzers/statistics/outliers.py +2 -2
  93. oscura/analyzers/statistics/streaming.py +30 -5
  94. oscura/analyzers/validation.py +216 -101
  95. oscura/analyzers/waveform/measurements.py +9 -0
  96. oscura/analyzers/waveform/measurements_with_uncertainty.py +31 -15
  97. oscura/analyzers/waveform/spectral.py +500 -228
  98. oscura/api/__init__.py +31 -5
  99. oscura/api/dsl/__init__.py +582 -0
  100. oscura/{dsl → api/dsl}/commands.py +43 -76
  101. oscura/{dsl → api/dsl}/interpreter.py +26 -51
  102. oscura/{dsl → api/dsl}/parser.py +107 -77
  103. oscura/{dsl → api/dsl}/repl.py +2 -2
  104. oscura/api/dsl.py +1 -1
  105. oscura/{integrations → api/integrations}/__init__.py +1 -1
  106. oscura/{integrations → api/integrations}/llm.py +201 -102
  107. oscura/api/operators.py +3 -3
  108. oscura/api/optimization.py +144 -30
  109. oscura/api/rest_server.py +921 -0
  110. oscura/api/server/__init__.py +17 -0
  111. oscura/api/server/dashboard.py +850 -0
  112. oscura/api/server/static/README.md +34 -0
  113. oscura/api/server/templates/base.html +181 -0
  114. oscura/api/server/templates/export.html +120 -0
  115. oscura/api/server/templates/home.html +284 -0
  116. oscura/api/server/templates/protocols.html +58 -0
  117. oscura/api/server/templates/reports.html +43 -0
  118. oscura/api/server/templates/session_detail.html +89 -0
  119. oscura/api/server/templates/sessions.html +83 -0
  120. oscura/api/server/templates/waveforms.html +73 -0
  121. oscura/automotive/__init__.py +8 -1
  122. oscura/automotive/can/__init__.py +10 -0
  123. oscura/automotive/can/checksum.py +3 -1
  124. oscura/automotive/can/dbc_generator.py +590 -0
  125. oscura/automotive/can/message_wrapper.py +121 -74
  126. oscura/automotive/can/patterns.py +98 -21
  127. oscura/automotive/can/session.py +292 -56
  128. oscura/automotive/can/state_machine.py +6 -3
  129. oscura/automotive/can/stimulus_response.py +97 -75
  130. oscura/automotive/dbc/__init__.py +10 -2
  131. oscura/automotive/dbc/generator.py +84 -56
  132. oscura/automotive/dbc/parser.py +6 -6
  133. oscura/automotive/dtc/data.json +17 -102
  134. oscura/automotive/dtc/database.py +2 -2
  135. oscura/automotive/flexray/__init__.py +31 -0
  136. oscura/automotive/flexray/analyzer.py +504 -0
  137. oscura/automotive/flexray/crc.py +185 -0
  138. oscura/automotive/flexray/fibex.py +449 -0
  139. oscura/automotive/j1939/__init__.py +45 -8
  140. oscura/automotive/j1939/analyzer.py +605 -0
  141. oscura/automotive/j1939/spns.py +326 -0
  142. oscura/automotive/j1939/transport.py +306 -0
  143. oscura/automotive/lin/__init__.py +47 -0
  144. oscura/automotive/lin/analyzer.py +612 -0
  145. oscura/automotive/loaders/blf.py +13 -2
  146. oscura/automotive/loaders/csv_can.py +143 -72
  147. oscura/automotive/loaders/dispatcher.py +50 -2
  148. oscura/automotive/loaders/mdf.py +86 -45
  149. oscura/automotive/loaders/pcap.py +111 -61
  150. oscura/automotive/uds/__init__.py +4 -0
  151. oscura/automotive/uds/analyzer.py +725 -0
  152. oscura/automotive/uds/decoder.py +140 -58
  153. oscura/automotive/uds/models.py +7 -1
  154. oscura/automotive/visualization.py +1 -1
  155. oscura/cli/analyze.py +348 -0
  156. oscura/cli/batch.py +142 -122
  157. oscura/cli/benchmark.py +275 -0
  158. oscura/cli/characterize.py +137 -82
  159. oscura/cli/compare.py +224 -131
  160. oscura/cli/completion.py +250 -0
  161. oscura/cli/config_cmd.py +361 -0
  162. oscura/cli/decode.py +164 -87
  163. oscura/cli/export.py +286 -0
  164. oscura/cli/main.py +115 -31
  165. oscura/{onboarding → cli/onboarding}/__init__.py +3 -3
  166. oscura/{onboarding → cli/onboarding}/help.py +80 -58
  167. oscura/{onboarding → cli/onboarding}/tutorials.py +97 -72
  168. oscura/{onboarding → cli/onboarding}/wizard.py +55 -36
  169. oscura/cli/progress.py +147 -0
  170. oscura/cli/shell.py +157 -135
  171. oscura/cli/validate_cmd.py +204 -0
  172. oscura/cli/visualize.py +158 -0
  173. oscura/convenience.py +125 -79
  174. oscura/core/__init__.py +4 -2
  175. oscura/core/backend_selector.py +3 -3
  176. oscura/core/cache.py +126 -15
  177. oscura/core/cancellation.py +1 -1
  178. oscura/{config → core/config}/__init__.py +20 -11
  179. oscura/{config → core/config}/defaults.py +1 -1
  180. oscura/{config → core/config}/loader.py +7 -5
  181. oscura/{config → core/config}/memory.py +5 -5
  182. oscura/{config → core/config}/migration.py +1 -1
  183. oscura/{config → core/config}/pipeline.py +99 -23
  184. oscura/{config → core/config}/preferences.py +1 -1
  185. oscura/{config → core/config}/protocol.py +3 -3
  186. oscura/{config → core/config}/schema.py +426 -272
  187. oscura/{config → core/config}/settings.py +1 -1
  188. oscura/{config → core/config}/thresholds.py +195 -153
  189. oscura/core/correlation.py +5 -6
  190. oscura/core/cross_domain.py +0 -2
  191. oscura/core/debug.py +9 -5
  192. oscura/{extensibility → core/extensibility}/docs.py +158 -70
  193. oscura/{extensibility → core/extensibility}/extensions.py +160 -76
  194. oscura/{extensibility → core/extensibility}/logging.py +1 -1
  195. oscura/{extensibility → core/extensibility}/measurements.py +1 -1
  196. oscura/{extensibility → core/extensibility}/plugins.py +1 -1
  197. oscura/{extensibility → core/extensibility}/templates.py +73 -3
  198. oscura/{extensibility → core/extensibility}/validation.py +1 -1
  199. oscura/core/gpu_backend.py +11 -7
  200. oscura/core/log_query.py +101 -11
  201. oscura/core/logging.py +126 -54
  202. oscura/core/logging_advanced.py +5 -5
  203. oscura/core/memory_limits.py +108 -70
  204. oscura/core/memory_monitor.py +2 -2
  205. oscura/core/memory_progress.py +7 -7
  206. oscura/core/memory_warnings.py +1 -1
  207. oscura/core/numba_backend.py +13 -13
  208. oscura/{plugins → core/plugins}/__init__.py +9 -9
  209. oscura/{plugins → core/plugins}/base.py +7 -7
  210. oscura/{plugins → core/plugins}/cli.py +3 -3
  211. oscura/{plugins → core/plugins}/discovery.py +186 -106
  212. oscura/{plugins → core/plugins}/lifecycle.py +1 -1
  213. oscura/{plugins → core/plugins}/manager.py +7 -7
  214. oscura/{plugins → core/plugins}/registry.py +3 -3
  215. oscura/{plugins → core/plugins}/versioning.py +1 -1
  216. oscura/core/progress.py +16 -1
  217. oscura/core/provenance.py +8 -2
  218. oscura/{schemas → core/schemas}/__init__.py +2 -2
  219. oscura/{schemas → core/schemas}/device_mapping.json +2 -8
  220. oscura/{schemas → core/schemas}/packet_format.json +4 -24
  221. oscura/{schemas → core/schemas}/protocol_definition.json +2 -12
  222. oscura/core/types.py +4 -0
  223. oscura/core/uncertainty.py +3 -3
  224. oscura/correlation/__init__.py +52 -0
  225. oscura/correlation/multi_protocol.py +811 -0
  226. oscura/discovery/auto_decoder.py +117 -35
  227. oscura/discovery/comparison.py +191 -86
  228. oscura/discovery/quality_validator.py +155 -68
  229. oscura/discovery/signal_detector.py +196 -79
  230. oscura/export/__init__.py +18 -8
  231. oscura/export/kaitai_struct.py +513 -0
  232. oscura/export/scapy_layer.py +801 -0
  233. oscura/export/wireshark/generator.py +1 -1
  234. oscura/export/wireshark/templates/dissector.lua.j2 +2 -2
  235. oscura/export/wireshark_dissector.py +746 -0
  236. oscura/guidance/wizard.py +207 -111
  237. oscura/hardware/__init__.py +19 -0
  238. oscura/{acquisition → hardware/acquisition}/__init__.py +4 -4
  239. oscura/{acquisition → hardware/acquisition}/file.py +2 -2
  240. oscura/{acquisition → hardware/acquisition}/hardware.py +7 -7
  241. oscura/{acquisition → hardware/acquisition}/saleae.py +15 -12
  242. oscura/{acquisition → hardware/acquisition}/socketcan.py +1 -1
  243. oscura/{acquisition → hardware/acquisition}/streaming.py +2 -2
  244. oscura/{acquisition → hardware/acquisition}/synthetic.py +3 -3
  245. oscura/{acquisition → hardware/acquisition}/visa.py +33 -11
  246. oscura/hardware/firmware/__init__.py +29 -0
  247. oscura/hardware/firmware/pattern_recognition.py +874 -0
  248. oscura/hardware/hal_detector.py +736 -0
  249. oscura/hardware/security/__init__.py +37 -0
  250. oscura/hardware/security/side_channel_detector.py +1126 -0
  251. oscura/inference/__init__.py +4 -0
  252. oscura/inference/active_learning/observation_table.py +4 -1
  253. oscura/inference/alignment.py +216 -123
  254. oscura/inference/bayesian.py +113 -33
  255. oscura/inference/crc_reverse.py +101 -55
  256. oscura/inference/logic.py +6 -2
  257. oscura/inference/message_format.py +342 -183
  258. oscura/inference/protocol.py +95 -44
  259. oscura/inference/protocol_dsl.py +180 -82
  260. oscura/inference/signal_intelligence.py +1439 -706
  261. oscura/inference/spectral.py +99 -57
  262. oscura/inference/state_machine.py +810 -158
  263. oscura/inference/stream.py +270 -110
  264. oscura/iot/__init__.py +34 -0
  265. oscura/iot/coap/__init__.py +32 -0
  266. oscura/iot/coap/analyzer.py +668 -0
  267. oscura/iot/coap/options.py +212 -0
  268. oscura/iot/lorawan/__init__.py +21 -0
  269. oscura/iot/lorawan/crypto.py +206 -0
  270. oscura/iot/lorawan/decoder.py +801 -0
  271. oscura/iot/lorawan/mac_commands.py +341 -0
  272. oscura/iot/mqtt/__init__.py +27 -0
  273. oscura/iot/mqtt/analyzer.py +999 -0
  274. oscura/iot/mqtt/properties.py +315 -0
  275. oscura/iot/zigbee/__init__.py +31 -0
  276. oscura/iot/zigbee/analyzer.py +615 -0
  277. oscura/iot/zigbee/security.py +153 -0
  278. oscura/iot/zigbee/zcl.py +349 -0
  279. oscura/jupyter/display.py +125 -45
  280. oscura/{exploratory → jupyter/exploratory}/__init__.py +8 -8
  281. oscura/{exploratory → jupyter/exploratory}/error_recovery.py +298 -141
  282. oscura/jupyter/exploratory/fuzzy.py +746 -0
  283. oscura/{exploratory → jupyter/exploratory}/fuzzy_advanced.py +258 -100
  284. oscura/{exploratory → jupyter/exploratory}/legacy.py +464 -242
  285. oscura/{exploratory → jupyter/exploratory}/parse.py +167 -145
  286. oscura/{exploratory → jupyter/exploratory}/recovery.py +119 -87
  287. oscura/jupyter/exploratory/sync.py +612 -0
  288. oscura/{exploratory → jupyter/exploratory}/unknown.py +299 -176
  289. oscura/jupyter/magic.py +4 -4
  290. oscura/{ui → jupyter/ui}/__init__.py +2 -2
  291. oscura/{ui → jupyter/ui}/formatters.py +3 -3
  292. oscura/{ui → jupyter/ui}/progressive_display.py +153 -82
  293. oscura/loaders/__init__.py +183 -67
  294. oscura/loaders/binary.py +88 -1
  295. oscura/loaders/chipwhisperer.py +153 -137
  296. oscura/loaders/configurable.py +208 -86
  297. oscura/loaders/csv_loader.py +458 -215
  298. oscura/loaders/hdf5_loader.py +278 -119
  299. oscura/loaders/lazy.py +87 -54
  300. oscura/loaders/mmap_loader.py +1 -1
  301. oscura/loaders/numpy_loader.py +253 -116
  302. oscura/loaders/pcap.py +226 -151
  303. oscura/loaders/rigol.py +110 -49
  304. oscura/loaders/sigrok.py +201 -78
  305. oscura/loaders/tdms.py +81 -58
  306. oscura/loaders/tektronix.py +291 -174
  307. oscura/loaders/touchstone.py +182 -87
  308. oscura/loaders/tss.py +456 -0
  309. oscura/loaders/vcd.py +215 -117
  310. oscura/loaders/wav.py +155 -68
  311. oscura/reporting/__init__.py +9 -0
  312. oscura/reporting/analyze.py +352 -146
  313. oscura/reporting/argument_preparer.py +69 -14
  314. oscura/reporting/auto_report.py +97 -61
  315. oscura/reporting/batch.py +131 -58
  316. oscura/reporting/chart_selection.py +57 -45
  317. oscura/reporting/comparison.py +63 -17
  318. oscura/reporting/content/executive.py +76 -24
  319. oscura/reporting/core_formats/multi_format.py +11 -8
  320. oscura/reporting/engine.py +312 -158
  321. oscura/reporting/enhanced_reports.py +949 -0
  322. oscura/reporting/export.py +86 -43
  323. oscura/reporting/formatting/numbers.py +69 -42
  324. oscura/reporting/html.py +139 -58
  325. oscura/reporting/index.py +137 -65
  326. oscura/reporting/output.py +158 -67
  327. oscura/reporting/pdf.py +67 -102
  328. oscura/reporting/plots.py +191 -112
  329. oscura/reporting/sections.py +88 -47
  330. oscura/reporting/standards.py +104 -61
  331. oscura/reporting/summary_generator.py +75 -55
  332. oscura/reporting/tables.py +138 -54
  333. oscura/reporting/templates/enhanced/protocol_re.html +525 -0
  334. oscura/sessions/__init__.py +14 -23
  335. oscura/sessions/base.py +3 -3
  336. oscura/sessions/blackbox.py +106 -10
  337. oscura/sessions/generic.py +2 -2
  338. oscura/sessions/legacy.py +783 -0
  339. oscura/side_channel/__init__.py +63 -0
  340. oscura/side_channel/dpa.py +1025 -0
  341. oscura/utils/__init__.py +15 -1
  342. oscura/utils/bitwise.py +118 -0
  343. oscura/{builders → utils/builders}/__init__.py +1 -1
  344. oscura/{comparison → utils/comparison}/__init__.py +6 -6
  345. oscura/{comparison → utils/comparison}/compare.py +202 -101
  346. oscura/{comparison → utils/comparison}/golden.py +83 -63
  347. oscura/{comparison → utils/comparison}/limits.py +313 -89
  348. oscura/{comparison → utils/comparison}/mask.py +151 -45
  349. oscura/{comparison → utils/comparison}/trace_diff.py +1 -1
  350. oscura/{comparison → utils/comparison}/visualization.py +147 -89
  351. oscura/{component → utils/component}/__init__.py +3 -3
  352. oscura/{component → utils/component}/impedance.py +122 -58
  353. oscura/{component → utils/component}/reactive.py +165 -168
  354. oscura/{component → utils/component}/transmission_line.py +3 -3
  355. oscura/{filtering → utils/filtering}/__init__.py +6 -6
  356. oscura/{filtering → utils/filtering}/base.py +1 -1
  357. oscura/{filtering → utils/filtering}/convenience.py +2 -2
  358. oscura/{filtering → utils/filtering}/design.py +169 -93
  359. oscura/{filtering → utils/filtering}/filters.py +2 -2
  360. oscura/{filtering → utils/filtering}/introspection.py +2 -2
  361. oscura/utils/geometry.py +31 -0
  362. oscura/utils/imports.py +184 -0
  363. oscura/utils/lazy.py +1 -1
  364. oscura/{math → utils/math}/__init__.py +2 -2
  365. oscura/{math → utils/math}/arithmetic.py +114 -48
  366. oscura/{math → utils/math}/interpolation.py +139 -106
  367. oscura/utils/memory.py +129 -66
  368. oscura/utils/memory_advanced.py +92 -9
  369. oscura/utils/memory_extensions.py +10 -8
  370. oscura/{optimization → utils/optimization}/__init__.py +1 -1
  371. oscura/{optimization → utils/optimization}/search.py +2 -2
  372. oscura/utils/performance/__init__.py +58 -0
  373. oscura/utils/performance/caching.py +889 -0
  374. oscura/utils/performance/lsh_clustering.py +333 -0
  375. oscura/utils/performance/memory_optimizer.py +699 -0
  376. oscura/utils/performance/optimizations.py +675 -0
  377. oscura/utils/performance/parallel.py +654 -0
  378. oscura/utils/performance/profiling.py +661 -0
  379. oscura/{pipeline → utils/pipeline}/base.py +1 -1
  380. oscura/{pipeline → utils/pipeline}/composition.py +1 -1
  381. oscura/{pipeline → utils/pipeline}/parallel.py +3 -2
  382. oscura/{pipeline → utils/pipeline}/pipeline.py +1 -1
  383. oscura/{pipeline → utils/pipeline}/reverse_engineering.py +412 -221
  384. oscura/{search → utils/search}/__init__.py +3 -3
  385. oscura/{search → utils/search}/anomaly.py +188 -58
  386. oscura/utils/search/context.py +294 -0
  387. oscura/{search → utils/search}/pattern.py +138 -10
  388. oscura/utils/serial.py +51 -0
  389. oscura/utils/storage/__init__.py +61 -0
  390. oscura/utils/storage/database.py +1166 -0
  391. oscura/{streaming → utils/streaming}/chunked.py +302 -143
  392. oscura/{streaming → utils/streaming}/progressive.py +1 -1
  393. oscura/{streaming → utils/streaming}/realtime.py +3 -2
  394. oscura/{triggering → utils/triggering}/__init__.py +6 -6
  395. oscura/{triggering → utils/triggering}/base.py +6 -6
  396. oscura/{triggering → utils/triggering}/edge.py +2 -2
  397. oscura/{triggering → utils/triggering}/pattern.py +2 -2
  398. oscura/{triggering → utils/triggering}/pulse.py +115 -74
  399. oscura/{triggering → utils/triggering}/window.py +2 -2
  400. oscura/utils/validation.py +32 -0
  401. oscura/validation/__init__.py +121 -0
  402. oscura/{compliance → validation/compliance}/__init__.py +5 -5
  403. oscura/{compliance → validation/compliance}/advanced.py +5 -5
  404. oscura/{compliance → validation/compliance}/masks.py +1 -1
  405. oscura/{compliance → validation/compliance}/reporting.py +127 -53
  406. oscura/{compliance → validation/compliance}/testing.py +114 -52
  407. oscura/validation/compliance_tests.py +915 -0
  408. oscura/validation/fuzzer.py +990 -0
  409. oscura/validation/grammar_tests.py +596 -0
  410. oscura/validation/grammar_validator.py +904 -0
  411. oscura/validation/hil_testing.py +977 -0
  412. oscura/{quality → validation/quality}/__init__.py +4 -4
  413. oscura/{quality → validation/quality}/ensemble.py +251 -171
  414. oscura/{quality → validation/quality}/explainer.py +3 -3
  415. oscura/{quality → validation/quality}/scoring.py +1 -1
  416. oscura/{quality → validation/quality}/warnings.py +4 -4
  417. oscura/validation/regression_suite.py +808 -0
  418. oscura/validation/replay.py +788 -0
  419. oscura/{testing → validation/testing}/__init__.py +2 -2
  420. oscura/{testing → validation/testing}/synthetic.py +5 -5
  421. oscura/visualization/__init__.py +9 -0
  422. oscura/visualization/accessibility.py +1 -1
  423. oscura/visualization/annotations.py +64 -67
  424. oscura/visualization/colors.py +7 -7
  425. oscura/visualization/digital.py +180 -81
  426. oscura/visualization/eye.py +236 -85
  427. oscura/visualization/interactive.py +320 -143
  428. oscura/visualization/jitter.py +587 -247
  429. oscura/visualization/layout.py +169 -134
  430. oscura/visualization/optimization.py +103 -52
  431. oscura/visualization/palettes.py +1 -1
  432. oscura/visualization/power.py +427 -211
  433. oscura/visualization/power_extended.py +626 -297
  434. oscura/visualization/presets.py +2 -0
  435. oscura/visualization/protocols.py +495 -181
  436. oscura/visualization/render.py +79 -63
  437. oscura/visualization/reverse_engineering.py +171 -124
  438. oscura/visualization/signal_integrity.py +460 -279
  439. oscura/visualization/specialized.py +190 -100
  440. oscura/visualization/spectral.py +670 -255
  441. oscura/visualization/thumbnails.py +166 -137
  442. oscura/visualization/waveform.py +150 -63
  443. oscura/workflows/__init__.py +3 -0
  444. oscura/{batch → workflows/batch}/__init__.py +5 -5
  445. oscura/{batch → workflows/batch}/advanced.py +150 -75
  446. oscura/workflows/batch/aggregate.py +531 -0
  447. oscura/workflows/batch/analyze.py +236 -0
  448. oscura/{batch → workflows/batch}/logging.py +2 -2
  449. oscura/{batch → workflows/batch}/metrics.py +1 -1
  450. oscura/workflows/complete_re.py +1144 -0
  451. oscura/workflows/compliance.py +44 -54
  452. oscura/workflows/digital.py +197 -51
  453. oscura/workflows/legacy/__init__.py +12 -0
  454. oscura/{workflow → workflows/legacy}/dag.py +4 -1
  455. oscura/workflows/multi_trace.py +9 -9
  456. oscura/workflows/power.py +42 -62
  457. oscura/workflows/protocol.py +82 -49
  458. oscura/workflows/reverse_engineering.py +351 -150
  459. oscura/workflows/signal_integrity.py +157 -82
  460. oscura-0.7.0.dist-info/METADATA +661 -0
  461. oscura-0.7.0.dist-info/RECORD +591 -0
  462. oscura/batch/aggregate.py +0 -300
  463. oscura/batch/analyze.py +0 -139
  464. oscura/dsl/__init__.py +0 -73
  465. oscura/exceptions.py +0 -59
  466. oscura/exploratory/fuzzy.py +0 -513
  467. oscura/exploratory/sync.py +0 -384
  468. oscura/exporters/__init__.py +0 -94
  469. oscura/exporters/csv.py +0 -303
  470. oscura/exporters/exporters.py +0 -44
  471. oscura/exporters/hdf5.py +0 -217
  472. oscura/exporters/html_export.py +0 -701
  473. oscura/exporters/json_export.py +0 -291
  474. oscura/exporters/markdown_export.py +0 -367
  475. oscura/exporters/matlab_export.py +0 -354
  476. oscura/exporters/npz_export.py +0 -219
  477. oscura/exporters/spice_export.py +0 -210
  478. oscura/search/context.py +0 -149
  479. oscura/session/__init__.py +0 -34
  480. oscura/session/annotations.py +0 -289
  481. oscura/session/history.py +0 -313
  482. oscura/session/session.py +0 -520
  483. oscura/workflow/__init__.py +0 -13
  484. oscura-0.5.1.dist-info/METADATA +0 -583
  485. oscura-0.5.1.dist-info/RECORD +0 -481
  486. /oscura/core/{config.py → config/legacy.py} +0 -0
  487. /oscura/{extensibility → core/extensibility}/__init__.py +0 -0
  488. /oscura/{extensibility → core/extensibility}/registry.py +0 -0
  489. /oscura/{plugins → core/plugins}/isolation.py +0 -0
  490. /oscura/{schemas → core/schemas}/bus_configuration.json +0 -0
  491. /oscura/{builders → utils/builders}/signal_builder.py +0 -0
  492. /oscura/{optimization → utils/optimization}/parallel.py +0 -0
  493. /oscura/{pipeline → utils/pipeline}/__init__.py +0 -0
  494. /oscura/{streaming → utils/streaming}/__init__.py +0 -0
  495. {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/WHEEL +0 -0
  496. {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/entry_points.txt +0 -0
  497. {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,999 @@
1
+ """MQTT protocol analyzer for versions 3.1.1 and 5.0.
2
+
3
+ This module provides comprehensive MQTT protocol analysis including packet
4
+ parsing, session tracking, and topic hierarchy discovery.
5
+
6
+ Example:
7
+ >>> from oscura.iot.mqtt import MQTTAnalyzer
8
+ >>> analyzer = MQTTAnalyzer()
9
+ >>> packet = analyzer.parse_packet(data, timestamp=0.0)
10
+ >>> topology = analyzer.get_topic_hierarchy()
11
+
12
+ References:
13
+ MQTT 3.1.1: http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/
14
+ MQTT 5.0: https://docs.oasis-open.org/mqtt/mqtt/v5.0/
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import json
20
+ from dataclasses import dataclass, field
21
+ from pathlib import Path
22
+ from typing import Any, ClassVar
23
+
24
+ from oscura.iot.mqtt.properties import parse_properties
25
+
26
+
27
+ @dataclass
28
+ class MQTTPacket:
29
+ """MQTT control packet representation.
30
+
31
+ Attributes:
32
+ timestamp: Packet timestamp in seconds.
33
+ packet_type: Packet type name ("CONNECT", "PUBLISH", etc.).
34
+ protocol_version: MQTT protocol version ("3.1.1" or "5.0").
35
+ flags: Packet flags (DUP, QoS, RETAIN, etc.).
36
+ packet_id: Packet identifier for QoS > 0 (optional).
37
+ topic: Topic name for PUBLISH/SUBSCRIBE packets (optional).
38
+ payload: Packet payload bytes.
39
+ properties: MQTT 5.0 properties dictionary.
40
+ qos: Quality of Service level (0, 1, or 2).
41
+
42
+ Example:
43
+ >>> packet = MQTTPacket(
44
+ ... timestamp=0.0,
45
+ ... packet_type="PUBLISH",
46
+ ... protocol_version="3.1.1",
47
+ ... flags={"dup": False, "qos": 0, "retain": False},
48
+ ... topic="home/sensor/temperature",
49
+ ... payload=b"22.5",
50
+ ... qos=0,
51
+ ... )
52
+ """
53
+
54
+ timestamp: float
55
+ packet_type: str
56
+ protocol_version: str
57
+ flags: dict[str, bool | int]
58
+ packet_id: int | None = None
59
+ topic: str | None = None
60
+ payload: bytes = b""
61
+ properties: dict[str, Any] = field(default_factory=dict)
62
+ qos: int = 0
63
+
64
+
65
+ @dataclass
66
+ class MQTTSession:
67
+ """MQTT session information.
68
+
69
+ Tracks connection details and topic subscriptions for a client.
70
+
71
+ Attributes:
72
+ client_id: Client identifier string.
73
+ username: Authentication username (optional).
74
+ protocol_version: MQTT protocol version ("3.1.1" or "5.0").
75
+ keep_alive: Keep-alive interval in seconds.
76
+ clean_session: Clean session flag (3.1.1) or clean start flag (5.0).
77
+ will_topic: Last Will and Testament topic (optional).
78
+ will_message: Last Will and Testament message payload (optional).
79
+ subscribed_topics: List of subscribed topic filters.
80
+ published_topics: Dictionary mapping topics to publish counts.
81
+
82
+ Example:
83
+ >>> session = MQTTSession(
84
+ ... client_id="sensor_01",
85
+ ... username="admin",
86
+ ... protocol_version="3.1.1",
87
+ ... keep_alive=60,
88
+ ... )
89
+ """
90
+
91
+ client_id: str
92
+ username: str | None = None
93
+ protocol_version: str = "3.1.1"
94
+ keep_alive: int = 60
95
+ clean_session: bool = True
96
+ will_topic: str | None = None
97
+ will_message: bytes | None = None
98
+ subscribed_topics: list[str] = field(default_factory=list)
99
+ published_topics: dict[str, int] = field(default_factory=dict)
100
+
101
+
102
+ class MQTTAnalyzer:
103
+ """MQTT protocol analyzer for versions 3.1.1 and 5.0.
104
+
105
+ Parses MQTT control packets, tracks sessions, and builds topic hierarchies.
106
+
107
+ Attributes:
108
+ PACKET_TYPES: Mapping of packet type codes to names.
109
+
110
+ Example:
111
+ >>> analyzer = MQTTAnalyzer()
112
+ >>> packet = analyzer.parse_packet(mqtt_data)
113
+ >>> hierarchy = analyzer.get_topic_hierarchy()
114
+ >>> analyzer.export_topology(Path("mqtt_topology.json"))
115
+ """
116
+
117
+ # MQTT control packet types
118
+ PACKET_TYPES: ClassVar[dict[int, str]] = {
119
+ 1: "CONNECT",
120
+ 2: "CONNACK",
121
+ 3: "PUBLISH",
122
+ 4: "PUBACK",
123
+ 5: "PUBREC",
124
+ 6: "PUBREL",
125
+ 7: "PUBCOMP",
126
+ 8: "SUBSCRIBE",
127
+ 9: "SUBACK",
128
+ 10: "UNSUBSCRIBE",
129
+ 11: "UNSUBACK",
130
+ 12: "PINGREQ",
131
+ 13: "PINGRESP",
132
+ 14: "DISCONNECT",
133
+ 15: "AUTH",
134
+ }
135
+
136
+ def __init__(self) -> None:
137
+ """Initialize MQTT analyzer.
138
+
139
+ Example:
140
+ >>> analyzer = MQTTAnalyzer()
141
+ >>> len(analyzer.packets)
142
+ 0
143
+ """
144
+ self.packets: list[MQTTPacket] = []
145
+ self.sessions: dict[str, MQTTSession] = {}
146
+ self.topics: set[str] = set()
147
+
148
+ def _calculate_header_size(self, data: bytes) -> int:
149
+ """Calculate fixed header size including length encoding.
150
+
151
+ Args:
152
+ data: Raw packet data
153
+
154
+ Returns:
155
+ Size of fixed header in bytes
156
+ """
157
+ header_size = 1
158
+ temp_data = data[1:]
159
+ while temp_data and (temp_data[0] & 0x80):
160
+ header_size += 1
161
+ temp_data = temp_data[1:]
162
+ return header_size + 1
163
+
164
+ def _parse_packet_data(
165
+ self, packet_type: str, var_header_payload: bytes, flags: int
166
+ ) -> dict[str, Any]:
167
+ """Parse variable header and payload based on packet type.
168
+
169
+ Args:
170
+ packet_type: Type of MQTT packet
171
+ var_header_payload: Variable header and payload bytes
172
+ flags: Packet flags
173
+
174
+ Returns:
175
+ Parsed packet data dictionary
176
+ """
177
+ parsers: dict[str, Any] = {
178
+ "CONNECT": lambda: self._parse_connect(var_header_payload),
179
+ "PUBLISH": lambda: self._parse_publish(var_header_payload, flags),
180
+ "SUBSCRIBE": lambda: self._parse_subscribe(var_header_payload),
181
+ "SUBACK": lambda: self._parse_suback(var_header_payload),
182
+ "UNSUBSCRIBE": lambda: self._parse_unsubscribe(var_header_payload),
183
+ "CONNACK": lambda: self._parse_connack(var_header_payload),
184
+ "DISCONNECT": lambda: self._parse_disconnect(var_header_payload),
185
+ "AUTH": lambda: self._parse_auth(var_header_payload),
186
+ }
187
+
188
+ if packet_type in parsers:
189
+ # Lambda returns dict[str, Any] from parser methods
190
+ from typing import cast
191
+
192
+ return cast("dict[str, Any]", parsers[packet_type]())
193
+
194
+ if packet_type in ["PUBACK", "PUBREC", "PUBREL", "PUBCOMP", "UNSUBACK"]:
195
+ return self._parse_ack(var_header_payload, packet_type)
196
+
197
+ if packet_type in ["PINGREQ", "PINGRESP"]:
198
+ return {}
199
+
200
+ return {"payload": var_header_payload}
201
+
202
+ def _track_packet_metadata(self, packet: MQTTPacket, parsed_data: dict[str, Any]) -> None:
203
+ """Track packet metadata (topics, sessions, subscriptions).
204
+
205
+ Args:
206
+ packet: Parsed MQTT packet
207
+ parsed_data: Raw parsed data
208
+ """
209
+ if packet.topic:
210
+ self.topics.add(packet.topic)
211
+
212
+ if packet.packet_type == "CONNECT" and "client_id" in parsed_data:
213
+ self._track_session(parsed_data)
214
+
215
+ if packet.packet_type == "SUBSCRIBE" and "topics" in parsed_data:
216
+ for topic_filter, _ in parsed_data["topics"]:
217
+ self.topics.add(topic_filter)
218
+
219
+ def parse_packet(self, data: bytes, timestamp: float = 0.0) -> MQTTPacket:
220
+ """Parse MQTT control packet.
221
+
222
+ Args:
223
+ data: Raw MQTT packet bytes.
224
+ timestamp: Packet timestamp in seconds.
225
+
226
+ Returns:
227
+ Parsed MQTT packet.
228
+
229
+ Raises:
230
+ ValueError: If packet is malformed or incomplete.
231
+
232
+ Example:
233
+ >>> analyzer = MQTTAnalyzer()
234
+ >>> # CONNECT packet
235
+ >>> data = b"\\x10\\x10\\x00\\x04MQTT\\x04\\x02\\x00\\x3c\\x00\\x04test"
236
+ >>> packet = analyzer.parse_packet(data)
237
+ >>> packet.packet_type
238
+ 'CONNECT'
239
+ """
240
+ if len(data) < 2:
241
+ raise ValueError("Insufficient data for MQTT packet")
242
+
243
+ # Parse fixed header and extract variable header/payload
244
+ packet_type, var_header_payload, flags = self._extract_packet_components(data)
245
+
246
+ # Parse variable header and payload
247
+ parsed_data = self._parse_packet_data(packet_type, var_header_payload, flags)
248
+
249
+ # Create packet object
250
+ packet = self._create_packet(timestamp, packet_type, parsed_data)
251
+
252
+ # Track metadata
253
+ self.packets.append(packet)
254
+ self._track_packet_metadata(packet, parsed_data)
255
+
256
+ return packet
257
+
258
+ def _extract_packet_components(self, data: bytes) -> tuple[str, bytes, int]:
259
+ """Extract packet type and variable header/payload from data.
260
+
261
+ Args:
262
+ data: Raw MQTT packet bytes.
263
+
264
+ Returns:
265
+ Tuple of (packet_type, var_header_payload, flags).
266
+
267
+ Raises:
268
+ ValueError: If packet is malformed or incomplete.
269
+ """
270
+ packet_type_code, flags, remaining_length = self._parse_fixed_header(data)
271
+
272
+ if packet_type_code not in self.PACKET_TYPES:
273
+ raise ValueError(f"Unknown packet type: {packet_type_code}")
274
+
275
+ packet_type = self.PACKET_TYPES[packet_type_code]
276
+ header_size = self._calculate_header_size(data)
277
+
278
+ if len(data) < header_size + remaining_length:
279
+ raise ValueError("Incomplete packet data")
280
+
281
+ var_header_payload = data[header_size : header_size + remaining_length]
282
+ return packet_type, var_header_payload, flags
283
+
284
+ def _create_packet(
285
+ self, timestamp: float, packet_type: str, parsed_data: dict[str, Any]
286
+ ) -> MQTTPacket:
287
+ """Create MQTTPacket from parsed data.
288
+
289
+ Args:
290
+ timestamp: Packet timestamp.
291
+ packet_type: Packet type string.
292
+ parsed_data: Parsed packet data dictionary.
293
+
294
+ Returns:
295
+ MQTTPacket instance.
296
+ """
297
+ return MQTTPacket(
298
+ timestamp=timestamp,
299
+ packet_type=packet_type,
300
+ protocol_version=parsed_data.get("protocol_version", "3.1.1"),
301
+ flags=parsed_data.get("flags", {}),
302
+ packet_id=parsed_data.get("packet_id"),
303
+ topic=parsed_data.get("topic"),
304
+ payload=parsed_data.get("payload", b""),
305
+ properties=parsed_data.get("properties", {}),
306
+ qos=parsed_data.get("qos", 0),
307
+ )
308
+
309
+ def _parse_fixed_header(self, data: bytes) -> tuple[int, int, int]:
310
+ """Parse MQTT fixed header.
311
+
312
+ The fixed header consists of:
313
+ - Byte 1: Control Packet type (bits 4-7) and Flags (bits 0-3)
314
+ - Byte 2+: Remaining Length (variable length encoding)
315
+
316
+ Args:
317
+ data: Raw packet data.
318
+
319
+ Returns:
320
+ Tuple of (packet_type, flags, remaining_length).
321
+
322
+ Raises:
323
+ ValueError: If header is malformed.
324
+
325
+ Example:
326
+ >>> analyzer = MQTTAnalyzer()
327
+ >>> # PUBLISH packet: type=3, flags=0, length=10
328
+ >>> data = b"\\x30\\x0a..."
329
+ >>> ptype, flags, length = analyzer._parse_fixed_header(data)
330
+ >>> ptype
331
+ 3
332
+ """
333
+ if len(data) < 2:
334
+ raise ValueError("Insufficient data for fixed header")
335
+
336
+ byte1 = data[0]
337
+ packet_type = (byte1 >> 4) & 0x0F
338
+ flags = byte1 & 0x0F
339
+
340
+ # Decode remaining length (variable byte integer)
341
+ multiplier = 1
342
+ value = 0
343
+ index = 1
344
+
345
+ while True:
346
+ if index >= len(data):
347
+ raise ValueError("Incomplete remaining length")
348
+
349
+ encoded_byte = data[index]
350
+ value += (encoded_byte & 0x7F) * multiplier
351
+
352
+ if (encoded_byte & 0x80) == 0:
353
+ break
354
+
355
+ multiplier *= 128
356
+ index += 1
357
+
358
+ if multiplier > 128 * 128 * 128:
359
+ raise ValueError("Remaining length exceeds maximum")
360
+
361
+ return packet_type, flags, value
362
+
363
+ def _parse_mqtt_string(self, data: bytes, offset: int) -> tuple[str, int]:
364
+ """Parse MQTT UTF-8 encoded string.
365
+
366
+ Args:
367
+ data: Packet data buffer.
368
+ offset: Current offset in buffer.
369
+
370
+ Returns:
371
+ Tuple of (parsed_string, new_offset).
372
+
373
+ Raises:
374
+ ValueError: If string data is incomplete.
375
+ """
376
+ if len(data) < offset + 2:
377
+ raise ValueError("Incomplete string length field")
378
+
379
+ str_len = int.from_bytes(data[offset : offset + 2], "big")
380
+ offset += 2
381
+
382
+ if len(data) < offset + str_len:
383
+ raise ValueError("Incomplete string data")
384
+
385
+ parsed_str = data[offset : offset + str_len].decode("utf-8")
386
+ return parsed_str, offset + str_len
387
+
388
+ def _parse_mqtt_binary(self, data: bytes, offset: int) -> tuple[bytes, int]:
389
+ """Parse MQTT binary data field.
390
+
391
+ Args:
392
+ data: Packet data buffer.
393
+ offset: Current offset in buffer.
394
+
395
+ Returns:
396
+ Tuple of (binary_data, new_offset).
397
+
398
+ Raises:
399
+ ValueError: If binary data is incomplete.
400
+ """
401
+ if len(data) < offset + 2:
402
+ raise ValueError("Incomplete binary length field")
403
+
404
+ bin_len = int.from_bytes(data[offset : offset + 2], "big")
405
+ offset += 2
406
+
407
+ if len(data) < offset + bin_len:
408
+ raise ValueError("Incomplete binary data")
409
+
410
+ binary_data = data[offset : offset + bin_len]
411
+ return binary_data, offset + bin_len
412
+
413
+ def _parse_connect_flags(self, connect_flags: int) -> dict[str, bool | int]:
414
+ """Parse CONNECT packet flags byte.
415
+
416
+ Args:
417
+ connect_flags: Flags byte from CONNECT packet.
418
+
419
+ Returns:
420
+ Dictionary of parsed flag values.
421
+ """
422
+ return {
423
+ "clean_session": bool(connect_flags & 0x02),
424
+ "will_flag": bool(connect_flags & 0x04),
425
+ "will_qos": (connect_flags >> 3) & 0x03,
426
+ "will_retain": bool(connect_flags & 0x20),
427
+ "username_flag": bool(connect_flags & 0x40),
428
+ "password_flag": bool(connect_flags & 0x80),
429
+ }
430
+
431
+ def _parse_will_data(
432
+ self, data: bytes, offset: int, protocol_version: str
433
+ ) -> tuple[str, bytes, int]:
434
+ """Parse Will topic and message from CONNECT packet.
435
+
436
+ Args:
437
+ data: Packet data buffer.
438
+ offset: Current offset in buffer.
439
+ protocol_version: MQTT protocol version.
440
+
441
+ Returns:
442
+ Tuple of (will_topic, will_message, new_offset).
443
+
444
+ Raises:
445
+ ValueError: If will data is incomplete.
446
+ """
447
+ # Will properties (MQTT 5.0 only)
448
+ if protocol_version == "5.0":
449
+ _, consumed = parse_properties(data, offset)
450
+ offset += consumed
451
+
452
+ # Will topic
453
+ will_topic, offset = self._parse_mqtt_string(data, offset)
454
+
455
+ # Will message
456
+ will_message, offset = self._parse_mqtt_binary(data, offset)
457
+
458
+ return will_topic, will_message, offset
459
+
460
+ def _parse_connect(self, data: bytes) -> dict[str, Any]:
461
+ """Parse CONNECT packet.
462
+
463
+ Args:
464
+ data: Variable header and payload bytes.
465
+
466
+ Returns:
467
+ Parsed CONNECT fields.
468
+
469
+ Raises:
470
+ ValueError: If packet is malformed.
471
+
472
+ Example:
473
+ >>> analyzer = MQTTAnalyzer()
474
+ >>> # MQTT 3.1.1 CONNECT
475
+ >>> data = b"\\x00\\x04MQTT\\x04\\x02\\x00\\x3c\\x00\\x04test"
476
+ >>> result = analyzer._parse_connect(data)
477
+ >>> result["protocol_version"]
478
+ '3.1.1'
479
+ """
480
+ if len(data) < 10:
481
+ raise ValueError("Insufficient data for CONNECT packet")
482
+
483
+ offset = 0
484
+
485
+ # Protocol name
486
+ protocol_name, offset = self._parse_mqtt_string(data, offset)
487
+
488
+ # Protocol level
489
+ protocol_level = data[offset]
490
+ offset += 1
491
+
492
+ # Map protocol level to version
493
+ version_map = {3: "3.1", 4: "3.1.1", 5: "5.0"}
494
+ protocol_version = version_map.get(protocol_level, "3.1.1")
495
+
496
+ # Connect flags
497
+ connect_flags = data[offset]
498
+ offset += 1
499
+ flags = self._parse_connect_flags(connect_flags)
500
+
501
+ # Keep alive
502
+ keep_alive = int.from_bytes(data[offset : offset + 2], "big")
503
+ offset += 2
504
+
505
+ # MQTT 5.0 properties
506
+ properties: dict[str, Any] = {}
507
+ if protocol_version == "5.0":
508
+ properties, consumed = parse_properties(data, offset)
509
+ offset += consumed
510
+
511
+ # Client ID
512
+ client_id, offset = self._parse_mqtt_string(data, offset)
513
+
514
+ # Will topic and message
515
+ will_topic = None
516
+ will_message = None
517
+ if flags["will_flag"]:
518
+ will_topic, will_message, offset = self._parse_will_data(data, offset, protocol_version)
519
+
520
+ # Username
521
+ username = None
522
+ if flags["username_flag"]:
523
+ username, offset = self._parse_mqtt_string(data, offset)
524
+
525
+ # Password
526
+ password = None
527
+ if flags["password_flag"]:
528
+ password, offset = self._parse_mqtt_binary(data, offset)
529
+
530
+ return {
531
+ "protocol_name": protocol_name,
532
+ "protocol_version": protocol_version,
533
+ "flags": flags,
534
+ "keep_alive": keep_alive,
535
+ "properties": properties,
536
+ "client_id": client_id,
537
+ "will_topic": will_topic,
538
+ "will_message": will_message,
539
+ "username": username,
540
+ "password": password,
541
+ }
542
+
543
+ def _parse_publish(self, data: bytes, flags: int) -> dict[str, Any]:
544
+ """Parse PUBLISH packet.
545
+
546
+ Args:
547
+ data: Variable header and payload bytes.
548
+ flags: Fixed header flags byte.
549
+
550
+ Returns:
551
+ Parsed PUBLISH fields.
552
+
553
+ Raises:
554
+ ValueError: If packet is malformed.
555
+
556
+ Example:
557
+ >>> analyzer = MQTTAnalyzer()
558
+ >>> # PUBLISH to "test" with QoS 0
559
+ >>> data = b"\\x00\\x04test22.5"
560
+ >>> result = analyzer._parse_publish(data, 0x00)
561
+ >>> result["topic"]
562
+ 'test'
563
+ """
564
+ qos = (flags >> 1) & 0x03
565
+ dup = bool(flags & 0x08)
566
+ retain = bool(flags & 0x01)
567
+
568
+ if len(data) < 2:
569
+ raise ValueError("Insufficient data for topic name length")
570
+
571
+ # Parse topic name
572
+ topic_len = int.from_bytes(data[0:2], "big")
573
+ if len(data) < 2 + topic_len:
574
+ raise ValueError("Insufficient data for topic name")
575
+
576
+ topic = data[2 : 2 + topic_len].decode("utf-8")
577
+ offset = 2 + topic_len
578
+
579
+ # Parse packet identifier (if QoS > 0)
580
+ packet_id = None
581
+ if qos > 0:
582
+ if len(data) < offset + 2:
583
+ raise ValueError("Insufficient data for packet ID")
584
+ packet_id = int.from_bytes(data[offset : offset + 2], "big")
585
+ offset += 2
586
+
587
+ # Parse properties (MQTT 5.0) - detect by checking if next byte looks like property length
588
+ properties: dict[str, Any] = {}
589
+ # We'll skip property parsing here for simplicity in 3.1.1 mode
590
+ # In 5.0, properties would be parsed before payload
591
+
592
+ # Remaining data is payload
593
+ payload = data[offset:]
594
+
595
+ return {
596
+ "topic": topic,
597
+ "qos": qos,
598
+ "flags": {"dup": dup, "qos": qos, "retain": retain},
599
+ "packet_id": packet_id,
600
+ "payload": payload,
601
+ "properties": properties,
602
+ }
603
+
604
+ def _parse_subscribe(self, data: bytes) -> dict[str, Any]:
605
+ """Parse SUBSCRIBE packet.
606
+
607
+ Args:
608
+ data: Variable header and payload bytes.
609
+
610
+ Returns:
611
+ Parsed SUBSCRIBE fields with topic filters.
612
+
613
+ Raises:
614
+ ValueError: If packet is malformed.
615
+
616
+ Example:
617
+ >>> analyzer = MQTTAnalyzer()
618
+ >>> # SUBSCRIBE to "test/+" with QoS 1
619
+ >>> data = b"\\x00\\x01\\x00\\x06test/+\\x01"
620
+ >>> result = analyzer._parse_subscribe(data)
621
+ >>> result["topics"][0]
622
+ ('test/+', 1)
623
+ """
624
+ if len(data) < 2:
625
+ raise ValueError("Insufficient data for packet ID")
626
+
627
+ # Packet identifier
628
+ packet_id = int.from_bytes(data[0:2], "big")
629
+ offset = 2
630
+
631
+ # Properties (MQTT 5.0) - skip for simplicity
632
+ properties: dict[str, Any] = {}
633
+
634
+ # Topic filters
635
+ topics = []
636
+ while offset < len(data):
637
+ if len(data) < offset + 2:
638
+ raise ValueError("Incomplete topic filter")
639
+
640
+ topic_len = int.from_bytes(data[offset : offset + 2], "big")
641
+ offset += 2
642
+
643
+ if len(data) < offset + topic_len:
644
+ raise ValueError("Incomplete topic filter")
645
+
646
+ topic_filter = data[offset : offset + topic_len].decode("utf-8")
647
+ offset += topic_len
648
+
649
+ if offset >= len(data):
650
+ raise ValueError("Missing subscription options")
651
+
652
+ # Subscription options (QoS in lower 2 bits for 3.1.1)
653
+ options = data[offset]
654
+ qos = options & 0x03
655
+ offset += 1
656
+
657
+ topics.append((topic_filter, qos))
658
+
659
+ return {
660
+ "packet_id": packet_id,
661
+ "properties": properties,
662
+ "topics": topics,
663
+ }
664
+
665
+ def _parse_ack(self, data: bytes, packet_type: str) -> dict[str, Any]:
666
+ """Parse acknowledgment packets (PUBACK, PUBREC, PUBREL, PUBCOMP, UNSUBACK).
667
+
668
+ Args:
669
+ data: Variable header bytes.
670
+ packet_type: Type of acknowledgment packet.
671
+
672
+ Returns:
673
+ Parsed acknowledgment fields.
674
+
675
+ Raises:
676
+ ValueError: If packet is malformed.
677
+
678
+ Example:
679
+ >>> analyzer = MQTTAnalyzer()
680
+ >>> data = b"\\x00\\x01" # Packet ID 1
681
+ >>> result = analyzer._parse_ack(data, "PUBACK")
682
+ >>> result["packet_id"]
683
+ 1
684
+ """
685
+ if len(data) < 2:
686
+ raise ValueError(f"Insufficient data for {packet_type} packet ID")
687
+
688
+ packet_id = int.from_bytes(data[0:2], "big")
689
+
690
+ # MQTT 5.0 may have reason code and properties
691
+ reason_code = None
692
+ properties: dict[str, Any] = {}
693
+
694
+ if len(data) > 2:
695
+ reason_code = data[2]
696
+ if len(data) > 3:
697
+ properties, _ = parse_properties(data, 3)
698
+
699
+ return {
700
+ "packet_id": packet_id,
701
+ "reason_code": reason_code,
702
+ "properties": properties,
703
+ }
704
+
705
+ def _parse_suback(self, data: bytes) -> dict[str, Any]:
706
+ """Parse SUBACK packet.
707
+
708
+ Args:
709
+ data: Variable header and payload bytes.
710
+
711
+ Returns:
712
+ Parsed SUBACK fields with return codes.
713
+
714
+ Raises:
715
+ ValueError: If packet is malformed.
716
+
717
+ Example:
718
+ >>> analyzer = MQTTAnalyzer()
719
+ >>> data = b"\\x00\\x01\\x00\\x01" # Packet ID 1, QoS 0 and 1 granted
720
+ >>> result = analyzer._parse_suback(data)
721
+ >>> result["return_codes"]
722
+ [0, 1]
723
+ """
724
+ if len(data) < 2:
725
+ raise ValueError("Insufficient data for SUBACK packet ID")
726
+
727
+ packet_id = int.from_bytes(data[0:2], "big")
728
+ offset = 2
729
+
730
+ # Properties (MQTT 5.0)
731
+ properties: dict[str, Any] = {}
732
+
733
+ # Return codes
734
+ return_codes = list(data[offset:])
735
+
736
+ return {
737
+ "packet_id": packet_id,
738
+ "properties": properties,
739
+ "return_codes": return_codes,
740
+ }
741
+
742
+ def _parse_unsubscribe(self, data: bytes) -> dict[str, Any]:
743
+ """Parse UNSUBSCRIBE packet.
744
+
745
+ Args:
746
+ data: Variable header and payload bytes.
747
+
748
+ Returns:
749
+ Parsed UNSUBSCRIBE fields with topic filters.
750
+
751
+ Raises:
752
+ ValueError: If packet is malformed.
753
+
754
+ Example:
755
+ >>> analyzer = MQTTAnalyzer()
756
+ >>> data = b"\\x00\\x01\\x00\\x04test"
757
+ >>> result = analyzer._parse_unsubscribe(data)
758
+ >>> result["topics"]
759
+ ['test']
760
+ """
761
+ if len(data) < 2:
762
+ raise ValueError("Insufficient data for packet ID")
763
+
764
+ packet_id = int.from_bytes(data[0:2], "big")
765
+ offset = 2
766
+
767
+ # Properties (MQTT 5.0)
768
+ properties: dict[str, Any] = {}
769
+
770
+ # Topic filters
771
+ topics = []
772
+ while offset < len(data):
773
+ if len(data) < offset + 2:
774
+ raise ValueError("Incomplete topic filter")
775
+
776
+ topic_len = int.from_bytes(data[offset : offset + 2], "big")
777
+ offset += 2
778
+
779
+ if len(data) < offset + topic_len:
780
+ raise ValueError("Incomplete topic filter")
781
+
782
+ topic_filter = data[offset : offset + topic_len].decode("utf-8")
783
+ offset += topic_len
784
+
785
+ topics.append(topic_filter)
786
+
787
+ return {
788
+ "packet_id": packet_id,
789
+ "properties": properties,
790
+ "topics": topics,
791
+ }
792
+
793
+ def _parse_connack(self, data: bytes) -> dict[str, Any]:
794
+ """Parse CONNACK packet.
795
+
796
+ Args:
797
+ data: Variable header bytes.
798
+
799
+ Returns:
800
+ Parsed CONNACK fields.
801
+
802
+ Raises:
803
+ ValueError: If packet is malformed.
804
+
805
+ Example:
806
+ >>> analyzer = MQTTAnalyzer()
807
+ >>> data = b"\\x00\\x00" # Session present=0, return code=0 (success)
808
+ >>> result = analyzer._parse_connack(data)
809
+ >>> result["return_code"]
810
+ 0
811
+ """
812
+ if len(data) < 2:
813
+ raise ValueError("Insufficient data for CONNACK")
814
+
815
+ acknowledge_flags = data[0]
816
+ session_present = bool(acknowledge_flags & 0x01)
817
+
818
+ return_code = data[1]
819
+
820
+ # Properties (MQTT 5.0)
821
+ properties: dict[str, Any] = {}
822
+ if len(data) > 2:
823
+ properties, _ = parse_properties(data, 2)
824
+
825
+ return {
826
+ "flags": {"session_present": session_present},
827
+ "return_code": return_code,
828
+ "properties": properties,
829
+ }
830
+
831
+ def _parse_disconnect(self, data: bytes) -> dict[str, Any]:
832
+ """Parse DISCONNECT packet.
833
+
834
+ Args:
835
+ data: Variable header bytes.
836
+
837
+ Returns:
838
+ Parsed DISCONNECT fields.
839
+
840
+ Example:
841
+ >>> analyzer = MQTTAnalyzer()
842
+ >>> data = b"" # Empty for MQTT 3.1.1
843
+ >>> result = analyzer._parse_disconnect(data)
844
+ >>> result
845
+ {}
846
+ """
847
+ # MQTT 3.1.1 has no variable header
848
+ if len(data) == 0:
849
+ return {}
850
+
851
+ # MQTT 5.0 has reason code and properties
852
+ reason_code = data[0] if len(data) > 0 else 0
853
+ properties: dict[str, Any] = {}
854
+
855
+ if len(data) > 1:
856
+ properties, _ = parse_properties(data, 1)
857
+
858
+ return {
859
+ "reason_code": reason_code,
860
+ "properties": properties,
861
+ }
862
+
863
+ def _parse_auth(self, data: bytes) -> dict[str, Any]:
864
+ """Parse AUTH packet (MQTT 5.0 only).
865
+
866
+ Args:
867
+ data: Variable header bytes.
868
+
869
+ Returns:
870
+ Parsed AUTH fields.
871
+
872
+ Raises:
873
+ ValueError: If packet is malformed.
874
+
875
+ Example:
876
+ >>> analyzer = MQTTAnalyzer()
877
+ >>> data = b"\\x00" # Reason code
878
+ >>> result = analyzer._parse_auth(data)
879
+ >>> result["reason_code"]
880
+ 0
881
+ """
882
+ if len(data) < 1:
883
+ raise ValueError("Insufficient data for AUTH")
884
+
885
+ reason_code = data[0]
886
+ properties: dict[str, Any] = {}
887
+
888
+ if len(data) > 1:
889
+ properties, _ = parse_properties(data, 1)
890
+
891
+ return {
892
+ "protocol_version": "5.0",
893
+ "reason_code": reason_code,
894
+ "properties": properties,
895
+ }
896
+
897
+ def _track_session(self, connect_data: dict[str, Any]) -> None:
898
+ """Track MQTT session from CONNECT packet.
899
+
900
+ Args:
901
+ connect_data: Parsed CONNECT packet data.
902
+
903
+ Example:
904
+ >>> analyzer = MQTTAnalyzer()
905
+ >>> connect_data = {
906
+ ... "client_id": "test",
907
+ ... "username": "admin",
908
+ ... "protocol_version": "3.1.1",
909
+ ... "keep_alive": 60,
910
+ ... "flags": {"clean_session": True},
911
+ ... }
912
+ >>> analyzer._track_session(connect_data)
913
+ >>> "test" in analyzer.sessions
914
+ True
915
+ """
916
+ client_id = connect_data["client_id"]
917
+
918
+ session = MQTTSession(
919
+ client_id=client_id,
920
+ username=connect_data.get("username"),
921
+ protocol_version=connect_data.get("protocol_version", "3.1.1"),
922
+ keep_alive=connect_data.get("keep_alive", 60),
923
+ clean_session=connect_data.get("flags", {}).get("clean_session", True),
924
+ will_topic=connect_data.get("will_topic"),
925
+ will_message=connect_data.get("will_message"),
926
+ )
927
+
928
+ self.sessions[client_id] = session
929
+
930
+ def get_topic_hierarchy(self) -> dict[str, Any]:
931
+ """Build hierarchical topic tree from observed topics.
932
+
933
+ Topics are split by '/' separator and organized into nested
934
+ dictionaries representing the topic hierarchy.
935
+
936
+ Returns:
937
+ Nested dictionary representing topic tree.
938
+
939
+ Example:
940
+ >>> analyzer = MQTTAnalyzer()
941
+ >>> analyzer.topics = {"home/sensor/temperature", "home/sensor/humidity"}
942
+ >>> tree = analyzer.get_topic_hierarchy()
943
+ >>> "home" in tree
944
+ True
945
+ >>> "sensor" in tree["home"]
946
+ True
947
+ """
948
+ tree: dict[str, Any] = {}
949
+
950
+ for topic in self.topics:
951
+ parts = topic.split("/")
952
+ current = tree
953
+
954
+ for part in parts:
955
+ if part not in current:
956
+ current[part] = {}
957
+ current = current[part]
958
+
959
+ return tree
960
+
961
+ def export_topology(self, output_path: Path) -> None:
962
+ """Export topic hierarchy and session information as JSON.
963
+
964
+ Args:
965
+ output_path: Path to output JSON file.
966
+
967
+ Example:
968
+ >>> analyzer = MQTTAnalyzer()
969
+ >>> analyzer.topics = {"test/topic"}
970
+ >>> analyzer.export_topology(Path("topology.json"))
971
+ """
972
+ topology = {
973
+ "topic_hierarchy": self.get_topic_hierarchy(),
974
+ "topics": sorted(self.topics),
975
+ "sessions": {
976
+ client_id: {
977
+ "client_id": session.client_id,
978
+ "username": session.username,
979
+ "protocol_version": session.protocol_version,
980
+ "keep_alive": session.keep_alive,
981
+ "clean_session": session.clean_session,
982
+ "will_topic": session.will_topic,
983
+ "subscribed_topics": session.subscribed_topics,
984
+ "published_topics": session.published_topics,
985
+ }
986
+ for client_id, session in self.sessions.items()
987
+ },
988
+ "packet_count": len(self.packets),
989
+ }
990
+
991
+ with output_path.open("w", encoding="utf-8") as f:
992
+ json.dump(topology, f, indent=2)
993
+
994
+
995
+ __all__ = [
996
+ "MQTTAnalyzer",
997
+ "MQTTPacket",
998
+ "MQTTSession",
999
+ ]