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,615 @@
1
+ """Zigbee protocol analyzer with network topology discovery.
2
+
3
+ This module provides comprehensive Zigbee protocol analysis including
4
+ NWK layer parsing, APS layer decoding, ZCL cluster support, and
5
+ network topology discovery.
6
+
7
+ Example:
8
+ >>> from oscura.iot.zigbee import ZigbeeAnalyzer, ZigbeeFrame
9
+ >>> analyzer = ZigbeeAnalyzer()
10
+ >>> frame = ZigbeeFrame(
11
+ ... timestamp=0.0,
12
+ ... frame_type="DATA",
13
+ ... source_address=0x1234,
14
+ ... dest_address=0x0000,
15
+ ... )
16
+ >>> analyzer.add_frame(frame)
17
+ >>> topology = analyzer.discover_topology()
18
+
19
+ References:
20
+ Zigbee Specification (CSA-IOT)
21
+ Zigbee NWK Layer Specification
22
+ Zigbee APS Layer Specification
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import json
28
+ from dataclasses import dataclass, field
29
+ from pathlib import Path
30
+ from typing import Any, ClassVar
31
+
32
+ from oscura.iot.zigbee.security import parse_security_header
33
+ from oscura.iot.zigbee.zcl import ZCL_CLUSTERS, parse_zcl_frame
34
+
35
+
36
+ @dataclass
37
+ class ZigbeeFrame:
38
+ """Zigbee frame representation.
39
+
40
+ Attributes:
41
+ timestamp: Frame timestamp in seconds.
42
+ frame_type: Frame type ("DATA", "COMMAND", "ACK").
43
+ source_address: 16-bit network address of source.
44
+ dest_address: 16-bit network address of destination.
45
+ source_ieee: 64-bit IEEE address of source (optional).
46
+ dest_ieee: 64-bit IEEE address of destination (optional).
47
+ sequence_number: NWK sequence number.
48
+ radius: Maximum hop count.
49
+ payload: Frame payload bytes.
50
+ decoded_aps: Decoded APS layer data (optional).
51
+ decoded_zcl: Decoded ZCL data (optional).
52
+
53
+ Example:
54
+ >>> frame = ZigbeeFrame(
55
+ ... timestamp=1.0,
56
+ ... frame_type="DATA",
57
+ ... source_address=0x1234,
58
+ ... dest_address=0x0000,
59
+ ... payload=b"\\x00\\x01\\x02",
60
+ ... )
61
+ """
62
+
63
+ timestamp: float
64
+ frame_type: str
65
+ source_address: int
66
+ dest_address: int
67
+ source_ieee: int | None = None
68
+ dest_ieee: int | None = None
69
+ sequence_number: int = 0
70
+ radius: int = 0
71
+ payload: bytes = b""
72
+ decoded_aps: dict[str, Any] | None = None
73
+ decoded_zcl: dict[str, Any] | None = None
74
+
75
+
76
+ @dataclass
77
+ class ZigbeeDevice:
78
+ """Zigbee device in network.
79
+
80
+ Attributes:
81
+ short_address: 16-bit network address.
82
+ ieee_address: 64-bit IEEE MAC address (optional).
83
+ device_type: Device type ("coordinator", "router", "end_device").
84
+ parent_address: Parent device address for routing (optional).
85
+ clusters: List of supported cluster IDs.
86
+ manufacturer: Manufacturer name (optional).
87
+ model: Model identifier (optional).
88
+
89
+ Example:
90
+ >>> device = ZigbeeDevice(
91
+ ... short_address=0x1234,
92
+ ... ieee_address=0x0013A20040A12345,
93
+ ... device_type="end_device",
94
+ ... clusters=[0x0006, 0x0008],
95
+ ... )
96
+ """
97
+
98
+ short_address: int
99
+ ieee_address: int | None = None
100
+ device_type: str = "unknown"
101
+ parent_address: int | None = None
102
+ clusters: list[int] = field(default_factory=list)
103
+ manufacturer: str | None = None
104
+ model: str | None = None
105
+
106
+
107
+ class ZigbeeAnalyzer:
108
+ """Zigbee protocol analyzer with ZCL cluster support.
109
+
110
+ Analyzes Zigbee network traffic including NWK/APS/ZCL layers,
111
+ discovers network topology, and supports security frame parsing.
112
+
113
+ Attributes:
114
+ STANDARD_CLUSTERS: Standard ZCL cluster ID to name mapping.
115
+ FRAME_TYPES: NWK frame type codes.
116
+
117
+ Example:
118
+ >>> analyzer = ZigbeeAnalyzer()
119
+ >>> analyzer.add_frame(frame)
120
+ >>> topology = analyzer.discover_topology()
121
+ >>> analyzer.export_topology(Path("network.json"))
122
+ """
123
+
124
+ # Standard ZCL cluster IDs
125
+ STANDARD_CLUSTERS: ClassVar[dict[int, str]] = ZCL_CLUSTERS
126
+
127
+ # Frame types
128
+ FRAME_TYPES: ClassVar[dict[int, str]] = {
129
+ 0x00: "DATA",
130
+ 0x01: "COMMAND",
131
+ 0x02: "ACK",
132
+ }
133
+
134
+ def __init__(self) -> None:
135
+ """Initialize Zigbee analyzer.
136
+
137
+ Example:
138
+ >>> analyzer = ZigbeeAnalyzer()
139
+ >>> len(analyzer.frames)
140
+ 0
141
+ """
142
+ self.frames: list[ZigbeeFrame] = []
143
+ self.devices: dict[int, ZigbeeDevice] = {}
144
+ self.network_keys: list[bytes] = []
145
+
146
+ def add_frame(self, frame: ZigbeeFrame) -> None:
147
+ """Add Zigbee frame for analysis.
148
+
149
+ Args:
150
+ frame: Zigbee frame to analyze.
151
+
152
+ Example:
153
+ >>> analyzer = ZigbeeAnalyzer()
154
+ >>> frame = ZigbeeFrame(
155
+ ... timestamp=0.0,
156
+ ... frame_type="DATA",
157
+ ... source_address=0x1234,
158
+ ... dest_address=0x0000,
159
+ ... )
160
+ >>> analyzer.add_frame(frame)
161
+ >>> len(analyzer.frames)
162
+ 1
163
+ """
164
+ self.frames.append(frame)
165
+
166
+ # Update device registry
167
+ if frame.source_address not in self.devices:
168
+ self.devices[frame.source_address] = ZigbeeDevice(
169
+ short_address=frame.source_address,
170
+ ieee_address=frame.source_ieee,
171
+ )
172
+ elif frame.source_ieee and not self.devices[frame.source_address].ieee_address:
173
+ self.devices[frame.source_address].ieee_address = frame.source_ieee
174
+
175
+ if frame.dest_address not in self.devices and frame.dest_address < 0xFFF0:
176
+ # Don't add broadcast addresses
177
+ self.devices[frame.dest_address] = ZigbeeDevice(
178
+ short_address=frame.dest_address,
179
+ ieee_address=frame.dest_ieee,
180
+ )
181
+
182
+ def parse_nwk_layer(self, data: bytes) -> dict[str, Any]:
183
+ """Parse Zigbee network layer frame.
184
+
185
+ Parses NWK frame structure including frame control, addresses,
186
+ sequence number, radius, and optional IEEE addresses.
187
+
188
+ Args:
189
+ data: Raw NWK layer frame bytes.
190
+
191
+ Returns:
192
+ Parsed NWK layer fields including addresses and payload.
193
+
194
+ Raises:
195
+ ValueError: If data is too short for NWK header.
196
+
197
+ Example:
198
+ >>> analyzer = ZigbeeAnalyzer()
199
+ >>> nwk_data = bytes([0x08, 0x00, 0x00, 0x00, 0x34, 0x12, 0x1E, 0x01])
200
+ >>> result = analyzer.parse_nwk_layer(nwk_data)
201
+ >>> result['source_address']
202
+ 4660
203
+ """
204
+ if len(data) < 8:
205
+ raise ValueError("Insufficient data for NWK header")
206
+
207
+ # Parse frame control and basic fields
208
+ frame_ctrl_data = self._parse_nwk_frame_control(data)
209
+
210
+ # Parse optional fields
211
+ offset, dest_ieee, src_ieee, security_data = self._parse_nwk_optional_fields(
212
+ data, frame_ctrl_data, offset=8
213
+ )
214
+
215
+ # Extract payload
216
+ payload = data[offset:]
217
+
218
+ return {
219
+ **frame_ctrl_data,
220
+ "dest_ieee": dest_ieee,
221
+ "source_ieee": src_ieee,
222
+ "security_data": security_data,
223
+ "payload": payload,
224
+ }
225
+
226
+ def _parse_nwk_frame_control(self, data: bytes) -> dict[str, Any]:
227
+ """Parse NWK frame control field and basic header.
228
+
229
+ Args:
230
+ data: Raw NWK frame bytes.
231
+
232
+ Returns:
233
+ Dictionary of frame control flags and basic fields.
234
+ """
235
+ frame_control = int.from_bytes(data[0:2], "little")
236
+ frame_type = (frame_control >> 0) & 0x03
237
+ protocol_version = (frame_control >> 2) & 0x0F
238
+ discover_route = (frame_control >> 6) & 0x03
239
+ multicast_flag = bool((frame_control >> 8) & 0x01)
240
+ security = bool((frame_control >> 9) & 0x01)
241
+ source_route = bool((frame_control >> 10) & 0x01)
242
+ dest_ieee_present = bool((frame_control >> 11) & 0x01)
243
+ src_ieee_present = bool((frame_control >> 12) & 0x01)
244
+
245
+ dest_addr = int.from_bytes(data[2:4], "little")
246
+ src_addr = int.from_bytes(data[4:6], "little")
247
+ radius = data[6]
248
+ sequence = data[7]
249
+
250
+ return {
251
+ "frame_type": self.FRAME_TYPES.get(frame_type, "UNKNOWN"),
252
+ "protocol_version": protocol_version,
253
+ "discover_route": discover_route,
254
+ "multicast": multicast_flag,
255
+ "security": security,
256
+ "source_route": source_route,
257
+ "dest_ieee_present": dest_ieee_present,
258
+ "src_ieee_present": src_ieee_present,
259
+ "dest_address": dest_addr,
260
+ "source_address": src_addr,
261
+ "radius": radius,
262
+ "sequence": sequence,
263
+ }
264
+
265
+ def _parse_nwk_optional_fields(
266
+ self, data: bytes, frame_ctrl: dict[str, Any], offset: int
267
+ ) -> tuple[int, int | None, int | None, dict[str, Any] | None]:
268
+ """Parse optional NWK fields (IEEE addresses, multicast, source route, security).
269
+
270
+ Args:
271
+ data: Raw NWK frame bytes.
272
+ frame_ctrl: Parsed frame control data.
273
+ offset: Current offset in data.
274
+
275
+ Returns:
276
+ Tuple of (new_offset, dest_ieee, src_ieee, security_data).
277
+
278
+ Raises:
279
+ ValueError: If data is insufficient for optional fields.
280
+ """
281
+ # Optional destination IEEE address
282
+ dest_ieee = None
283
+ if frame_ctrl["dest_ieee_present"]:
284
+ if len(data) < offset + 8:
285
+ raise ValueError("Insufficient data for destination IEEE address")
286
+ dest_ieee = int.from_bytes(data[offset : offset + 8], "little")
287
+ offset += 8
288
+
289
+ # Optional source IEEE address
290
+ src_ieee = None
291
+ if frame_ctrl["src_ieee_present"]:
292
+ if len(data) < offset + 8:
293
+ raise ValueError("Insufficient data for source IEEE address")
294
+ src_ieee = int.from_bytes(data[offset : offset + 8], "little")
295
+ offset += 8
296
+
297
+ # Multicast control
298
+ if frame_ctrl["multicast"]:
299
+ if len(data) < offset + 1:
300
+ raise ValueError("Insufficient data for multicast control")
301
+ offset += 1
302
+
303
+ # Source route subframe
304
+ if frame_ctrl["source_route"]:
305
+ if len(data) < offset + 1:
306
+ raise ValueError("Insufficient data for source route")
307
+ relay_count = data[offset]
308
+ offset += 2 + (relay_count * 2)
309
+
310
+ # Security header
311
+ security_data = None
312
+ if frame_ctrl["security"]:
313
+ if len(data) < offset + 5:
314
+ raise ValueError("Insufficient data for security header")
315
+ security_header = parse_security_header(data[offset:])
316
+ if "error" in security_header:
317
+ raise ValueError(security_header["error"])
318
+ security_data = security_header
319
+ offset += security_header["header_length"]
320
+
321
+ return offset, dest_ieee, src_ieee, security_data
322
+
323
+ def parse_aps_layer(self, data: bytes) -> dict[str, Any]:
324
+ """Parse Zigbee APS (Application Support) layer.
325
+
326
+ Parses APS frame including frame control, addressing, and cluster ID.
327
+
328
+ Args:
329
+ data: APS layer frame bytes.
330
+
331
+ Returns:
332
+ Parsed APS layer fields.
333
+
334
+ Raises:
335
+ ValueError: If data is too short for APS header.
336
+
337
+ Example:
338
+ >>> analyzer = ZigbeeAnalyzer()
339
+ >>> aps_data = bytes([0x00, 0x00, 0x06, 0x00, 0x01])
340
+ >>> result = analyzer.parse_aps_layer(aps_data)
341
+ >>> result['cluster_id']
342
+ 6
343
+ """
344
+ if len(data) < 3:
345
+ raise ValueError("Insufficient data for APS header")
346
+
347
+ # Parse frame control
348
+ frame_ctrl = self._parse_aps_frame_control(data[0])
349
+
350
+ # Parse addressing and IDs
351
+ offset, addressing_data = self._parse_aps_addressing(data, frame_ctrl, offset=1)
352
+
353
+ # Parse extended header if present
354
+ if frame_ctrl["extended_header"]:
355
+ if len(data) >= offset + 1:
356
+ offset += 1 # Skip extended frame control
357
+
358
+ payload = data[offset:]
359
+
360
+ cluster_id = addressing_data.get("cluster_id")
361
+ cluster_name = (
362
+ self.STANDARD_CLUSTERS.get(cluster_id, "Unknown")
363
+ if cluster_id is not None and isinstance(cluster_id, int)
364
+ else None
365
+ )
366
+
367
+ return {
368
+ **frame_ctrl,
369
+ **addressing_data,
370
+ "cluster_name": cluster_name,
371
+ "payload": payload,
372
+ }
373
+
374
+ def _parse_aps_frame_control(self, frame_control: int) -> dict[str, Any]:
375
+ """Parse APS frame control byte.
376
+
377
+ Args:
378
+ frame_control: Frame control byte value.
379
+
380
+ Returns:
381
+ Dictionary of frame control flags.
382
+ """
383
+ return {
384
+ "frame_type": frame_control & 0x03,
385
+ "delivery_mode": (frame_control >> 2) & 0x03,
386
+ "security": bool((frame_control >> 5) & 0x01),
387
+ "ack_request": bool((frame_control >> 6) & 0x01),
388
+ "extended_header": bool((frame_control >> 7) & 0x01),
389
+ }
390
+
391
+ def _parse_aps_addressing(
392
+ self, data: bytes, frame_ctrl: dict[str, Any], offset: int
393
+ ) -> tuple[int, dict[str, Any]]:
394
+ """Parse APS addressing fields (endpoints, cluster, profile).
395
+
396
+ Args:
397
+ data: APS frame bytes.
398
+ frame_ctrl: Parsed frame control data.
399
+ offset: Current offset in data.
400
+
401
+ Returns:
402
+ Tuple of (new_offset, addressing_data).
403
+
404
+ Raises:
405
+ ValueError: If data is insufficient for addressing fields.
406
+ """
407
+ addressing: dict[str, Any] = {
408
+ "dest_endpoint": None,
409
+ "group_address": None,
410
+ "cluster_id": None,
411
+ "profile_id": None,
412
+ "source_endpoint": None,
413
+ "aps_counter": None,
414
+ }
415
+
416
+ delivery_mode = frame_ctrl["delivery_mode"]
417
+
418
+ # Destination endpoint (for unicast/broadcast)
419
+ if delivery_mode in [0x00, 0x01, 0x02]:
420
+ if len(data) < offset + 1:
421
+ raise ValueError("Insufficient data for destination endpoint")
422
+ addressing["dest_endpoint"] = data[offset]
423
+ offset += 1
424
+
425
+ # Group address (for group delivery)
426
+ if delivery_mode == 0x01:
427
+ if len(data) < offset + 2:
428
+ raise ValueError("Insufficient data for group address")
429
+ addressing["group_address"] = int.from_bytes(data[offset : offset + 2], "little")
430
+ offset += 2
431
+
432
+ # Cluster ID
433
+ if len(data) >= offset + 2:
434
+ addressing["cluster_id"] = int.from_bytes(data[offset : offset + 2], "little")
435
+ offset += 2
436
+
437
+ # Profile ID
438
+ if len(data) >= offset + 2:
439
+ addressing["profile_id"] = int.from_bytes(data[offset : offset + 2], "little")
440
+ offset += 2
441
+
442
+ # Source endpoint
443
+ if len(data) >= offset + 1:
444
+ addressing["source_endpoint"] = data[offset]
445
+ offset += 1
446
+
447
+ # APS counter
448
+ if len(data) >= offset + 1:
449
+ addressing["aps_counter"] = data[offset]
450
+ offset += 1
451
+
452
+ return offset, addressing
453
+
454
+ def parse_zcl_frame(self, cluster_id: int, data: bytes) -> dict[str, Any]:
455
+ """Parse ZCL frame for specific cluster.
456
+
457
+ Wrapper around zcl.parse_zcl_frame for integration with analyzer.
458
+
459
+ Args:
460
+ cluster_id: ZCL cluster ID.
461
+ data: ZCL frame payload.
462
+
463
+ Returns:
464
+ Parsed ZCL frame data.
465
+
466
+ Example:
467
+ >>> analyzer = ZigbeeAnalyzer()
468
+ >>> zcl_data = bytes([0x01, 0x00, 0x01])
469
+ >>> result = analyzer.parse_zcl_frame(0x0006, zcl_data)
470
+ >>> result['command_name']
471
+ 'On'
472
+ """
473
+ return parse_zcl_frame(cluster_id, data)
474
+
475
+ def discover_topology(self) -> dict[int, list[int]]:
476
+ """Discover network topology from captured frames.
477
+
478
+ Analyzes frame routing to determine parent-child relationships
479
+ in the Zigbee network tree.
480
+
481
+ Returns:
482
+ Dictionary mapping parent addresses to list of child addresses.
483
+
484
+ Example:
485
+ >>> analyzer = ZigbeeAnalyzer()
486
+ >>> # Add frames...
487
+ >>> topology = analyzer.discover_topology()
488
+ >>> print(topology[0x0000]) # Coordinator's children
489
+ [4660, 8765]
490
+ """
491
+ topology: dict[int, list[int]] = {}
492
+
493
+ # Analyze frame patterns to infer topology
494
+ # Devices that communicate frequently with coordinator are likely direct children
495
+ # Devices that route through others are likely children of routers
496
+
497
+ for frame in self.frames:
498
+ # If a device sends to coordinator (0x0000), it might be a direct child
499
+ if frame.dest_address == 0x0000 and frame.source_address not in [0x0000]:
500
+ if 0x0000 not in topology:
501
+ topology[0x0000] = []
502
+ if frame.source_address not in topology[0x0000]:
503
+ topology[0x0000].append(frame.source_address)
504
+
505
+ # Infer device types from topology
506
+ for addr, device in self.devices.items():
507
+ if addr == 0x0000:
508
+ device.device_type = "coordinator"
509
+ elif addr in topology:
510
+ device.device_type = "router" # Has children
511
+ else:
512
+ device.device_type = "end_device" # No children
513
+
514
+ # Set parent address if known
515
+ for parent, children in topology.items():
516
+ if addr in children:
517
+ device.parent_address = parent
518
+
519
+ return topology
520
+
521
+ def add_network_key(self, key: bytes) -> None:
522
+ """Add network key for decrypting secured frames.
523
+
524
+ Args:
525
+ key: 128-bit (16-byte) AES network key.
526
+
527
+ Raises:
528
+ ValueError: If key is not 16 bytes.
529
+
530
+ Example:
531
+ >>> analyzer = ZigbeeAnalyzer()
532
+ >>> key = bytes([0x01] * 16)
533
+ >>> analyzer.add_network_key(key)
534
+ >>> len(analyzer.network_keys)
535
+ 1
536
+ """
537
+ if len(key) != 16:
538
+ raise ValueError(f"Network key must be 16 bytes, got {len(key)}")
539
+ self.network_keys.append(key)
540
+
541
+ def export_topology(self, output_path: Path) -> None:
542
+ """Export network topology as JSON with device information.
543
+
544
+ Exports topology data including devices, relationships, and
545
+ cluster information in JSON format. Also generates GraphViz
546
+ DOT format for visualization.
547
+
548
+ Args:
549
+ output_path: Path to output JSON file.
550
+
551
+ Example:
552
+ >>> analyzer = ZigbeeAnalyzer()
553
+ >>> # Add frames and analyze...
554
+ >>> analyzer.export_topology(Path("zigbee_network.json"))
555
+ """
556
+ topology = self.discover_topology()
557
+
558
+ export_data = {
559
+ "devices": {
560
+ addr: {
561
+ "short_address": f"0x{addr:04X}",
562
+ "ieee_address": f"0x{dev.ieee_address:016X}" if dev.ieee_address else None,
563
+ "device_type": dev.device_type,
564
+ "parent_address": f"0x{dev.parent_address:04X}" if dev.parent_address else None,
565
+ "clusters": [
566
+ {
567
+ "id": f"0x{cid:04X}",
568
+ "name": self.STANDARD_CLUSTERS.get(cid, "Unknown"),
569
+ }
570
+ for cid in dev.clusters
571
+ ],
572
+ "manufacturer": dev.manufacturer,
573
+ "model": dev.model,
574
+ }
575
+ for addr, dev in self.devices.items()
576
+ },
577
+ "topology": {
578
+ f"0x{parent:04X}": [f"0x{child:04X}" for child in children]
579
+ for parent, children in topology.items()
580
+ },
581
+ }
582
+
583
+ # Write JSON
584
+ with output_path.open("w") as f:
585
+ json.dump(export_data, f, indent=2)
586
+
587
+ # Generate GraphViz DOT file
588
+ dot_path = output_path.with_suffix(".dot")
589
+ with dot_path.open("w") as f:
590
+ f.write("digraph ZigbeeNetwork {\n")
591
+ f.write(" rankdir=TB;\n")
592
+ f.write(" node [shape=box];\n\n")
593
+
594
+ # Add nodes
595
+ for addr, dev in self.devices.items():
596
+ label = f"0x{addr:04X}\\n{dev.device_type}"
597
+ if dev.manufacturer:
598
+ label += f"\\n{dev.manufacturer}"
599
+ if dev.model:
600
+ label += f"\\n{dev.model}"
601
+
602
+ shape = "ellipse" if dev.device_type == "coordinator" else "box"
603
+ f.write(f' "0x{addr:04X}" [label="{label}", shape={shape}];\n')
604
+
605
+ f.write("\n")
606
+
607
+ # Add edges
608
+ for parent, children in topology.items():
609
+ for child in children:
610
+ f.write(f' "0x{parent:04X}" -> "0x{child:04X}";\n')
611
+
612
+ f.write("}\n")
613
+
614
+
615
+ __all__ = ["ZigbeeAnalyzer", "ZigbeeDevice", "ZigbeeFrame"]