oscura 0.5.0__py3-none-any.whl → 0.6.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 (513) 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/__init__.py +0 -48
  5. oscura/analyzers/digital/edges.py +325 -65
  6. oscura/analyzers/digital/extraction.py +0 -195
  7. oscura/analyzers/digital/quality.py +293 -166
  8. oscura/analyzers/digital/timing.py +260 -115
  9. oscura/analyzers/digital/timing_numba.py +334 -0
  10. oscura/analyzers/entropy.py +605 -0
  11. oscura/analyzers/eye/diagram.py +176 -109
  12. oscura/analyzers/eye/metrics.py +5 -5
  13. oscura/analyzers/jitter/__init__.py +6 -4
  14. oscura/analyzers/jitter/ber.py +52 -52
  15. oscura/analyzers/jitter/classification.py +156 -0
  16. oscura/analyzers/jitter/decomposition.py +163 -113
  17. oscura/analyzers/jitter/spectrum.py +80 -64
  18. oscura/analyzers/ml/__init__.py +39 -0
  19. oscura/analyzers/ml/features.py +600 -0
  20. oscura/analyzers/ml/signal_classifier.py +604 -0
  21. oscura/analyzers/packet/daq.py +246 -158
  22. oscura/analyzers/packet/parser.py +12 -1
  23. oscura/analyzers/packet/payload.py +50 -2110
  24. oscura/analyzers/packet/payload_analysis.py +361 -181
  25. oscura/analyzers/packet/payload_patterns.py +133 -70
  26. oscura/analyzers/packet/stream.py +84 -23
  27. oscura/analyzers/patterns/__init__.py +26 -5
  28. oscura/analyzers/patterns/anomaly_detection.py +908 -0
  29. oscura/analyzers/patterns/clustering.py +169 -108
  30. oscura/analyzers/patterns/clustering_optimized.py +227 -0
  31. oscura/analyzers/patterns/discovery.py +1 -1
  32. oscura/analyzers/patterns/matching.py +581 -197
  33. oscura/analyzers/patterns/pattern_mining.py +778 -0
  34. oscura/analyzers/patterns/periodic.py +121 -38
  35. oscura/analyzers/patterns/sequences.py +175 -78
  36. oscura/analyzers/power/conduction.py +1 -1
  37. oscura/analyzers/power/soa.py +6 -6
  38. oscura/analyzers/power/switching.py +250 -110
  39. oscura/analyzers/protocol/__init__.py +17 -1
  40. oscura/analyzers/protocols/__init__.py +1 -22
  41. oscura/analyzers/protocols/base.py +6 -6
  42. oscura/analyzers/protocols/ble/__init__.py +38 -0
  43. oscura/analyzers/protocols/ble/analyzer.py +809 -0
  44. oscura/analyzers/protocols/ble/uuids.py +288 -0
  45. oscura/analyzers/protocols/can.py +257 -127
  46. oscura/analyzers/protocols/can_fd.py +107 -80
  47. oscura/analyzers/protocols/flexray.py +139 -80
  48. oscura/analyzers/protocols/hdlc.py +93 -58
  49. oscura/analyzers/protocols/i2c.py +247 -106
  50. oscura/analyzers/protocols/i2s.py +138 -86
  51. oscura/analyzers/protocols/industrial/__init__.py +40 -0
  52. oscura/analyzers/protocols/industrial/bacnet/__init__.py +33 -0
  53. oscura/analyzers/protocols/industrial/bacnet/analyzer.py +708 -0
  54. oscura/analyzers/protocols/industrial/bacnet/encoding.py +412 -0
  55. oscura/analyzers/protocols/industrial/bacnet/services.py +622 -0
  56. oscura/analyzers/protocols/industrial/ethercat/__init__.py +30 -0
  57. oscura/analyzers/protocols/industrial/ethercat/analyzer.py +474 -0
  58. oscura/analyzers/protocols/industrial/ethercat/mailbox.py +339 -0
  59. oscura/analyzers/protocols/industrial/ethercat/topology.py +166 -0
  60. oscura/analyzers/protocols/industrial/modbus/__init__.py +31 -0
  61. oscura/analyzers/protocols/industrial/modbus/analyzer.py +525 -0
  62. oscura/analyzers/protocols/industrial/modbus/crc.py +79 -0
  63. oscura/analyzers/protocols/industrial/modbus/functions.py +436 -0
  64. oscura/analyzers/protocols/industrial/opcua/__init__.py +21 -0
  65. oscura/analyzers/protocols/industrial/opcua/analyzer.py +552 -0
  66. oscura/analyzers/protocols/industrial/opcua/datatypes.py +446 -0
  67. oscura/analyzers/protocols/industrial/opcua/services.py +264 -0
  68. oscura/analyzers/protocols/industrial/profinet/__init__.py +23 -0
  69. oscura/analyzers/protocols/industrial/profinet/analyzer.py +441 -0
  70. oscura/analyzers/protocols/industrial/profinet/dcp.py +263 -0
  71. oscura/analyzers/protocols/industrial/profinet/ptcp.py +200 -0
  72. oscura/analyzers/protocols/jtag.py +180 -98
  73. oscura/analyzers/protocols/lin.py +219 -114
  74. oscura/analyzers/protocols/manchester.py +4 -4
  75. oscura/analyzers/protocols/onewire.py +253 -149
  76. oscura/analyzers/protocols/parallel_bus/__init__.py +20 -0
  77. oscura/analyzers/protocols/parallel_bus/centronics.py +92 -0
  78. oscura/analyzers/protocols/parallel_bus/gpib.py +137 -0
  79. oscura/analyzers/protocols/spi.py +192 -95
  80. oscura/analyzers/protocols/swd.py +321 -167
  81. oscura/analyzers/protocols/uart.py +267 -125
  82. oscura/analyzers/protocols/usb.py +235 -131
  83. oscura/analyzers/side_channel/power.py +17 -12
  84. oscura/analyzers/signal/__init__.py +15 -0
  85. oscura/analyzers/signal/timing_analysis.py +1086 -0
  86. oscura/analyzers/signal_integrity/__init__.py +4 -1
  87. oscura/analyzers/signal_integrity/sparams.py +2 -19
  88. oscura/analyzers/spectral/chunked.py +129 -60
  89. oscura/analyzers/spectral/chunked_fft.py +300 -94
  90. oscura/analyzers/spectral/chunked_wavelet.py +100 -80
  91. oscura/analyzers/statistical/checksum.py +376 -217
  92. oscura/analyzers/statistical/classification.py +229 -107
  93. oscura/analyzers/statistical/entropy.py +78 -53
  94. oscura/analyzers/statistics/correlation.py +407 -211
  95. oscura/analyzers/statistics/outliers.py +2 -2
  96. oscura/analyzers/statistics/streaming.py +30 -5
  97. oscura/analyzers/validation.py +216 -101
  98. oscura/analyzers/waveform/measurements.py +9 -0
  99. oscura/analyzers/waveform/measurements_with_uncertainty.py +31 -15
  100. oscura/analyzers/waveform/spectral.py +500 -228
  101. oscura/api/__init__.py +31 -5
  102. oscura/api/dsl/__init__.py +582 -0
  103. oscura/{dsl → api/dsl}/commands.py +43 -76
  104. oscura/{dsl → api/dsl}/interpreter.py +26 -51
  105. oscura/{dsl → api/dsl}/parser.py +107 -77
  106. oscura/{dsl → api/dsl}/repl.py +2 -2
  107. oscura/api/dsl.py +1 -1
  108. oscura/{integrations → api/integrations}/__init__.py +1 -1
  109. oscura/{integrations → api/integrations}/llm.py +201 -102
  110. oscura/api/operators.py +3 -3
  111. oscura/api/optimization.py +144 -30
  112. oscura/api/rest_server.py +921 -0
  113. oscura/api/server/__init__.py +17 -0
  114. oscura/api/server/dashboard.py +850 -0
  115. oscura/api/server/static/README.md +34 -0
  116. oscura/api/server/templates/base.html +181 -0
  117. oscura/api/server/templates/export.html +120 -0
  118. oscura/api/server/templates/home.html +284 -0
  119. oscura/api/server/templates/protocols.html +58 -0
  120. oscura/api/server/templates/reports.html +43 -0
  121. oscura/api/server/templates/session_detail.html +89 -0
  122. oscura/api/server/templates/sessions.html +83 -0
  123. oscura/api/server/templates/waveforms.html +73 -0
  124. oscura/automotive/__init__.py +8 -1
  125. oscura/automotive/can/__init__.py +10 -0
  126. oscura/automotive/can/checksum.py +3 -1
  127. oscura/automotive/can/dbc_generator.py +590 -0
  128. oscura/automotive/can/message_wrapper.py +121 -74
  129. oscura/automotive/can/patterns.py +98 -21
  130. oscura/automotive/can/session.py +292 -56
  131. oscura/automotive/can/state_machine.py +6 -3
  132. oscura/automotive/can/stimulus_response.py +97 -75
  133. oscura/automotive/dbc/__init__.py +10 -2
  134. oscura/automotive/dbc/generator.py +84 -56
  135. oscura/automotive/dbc/parser.py +6 -6
  136. oscura/automotive/dtc/data.json +2763 -0
  137. oscura/automotive/dtc/database.py +2 -2
  138. oscura/automotive/flexray/__init__.py +31 -0
  139. oscura/automotive/flexray/analyzer.py +504 -0
  140. oscura/automotive/flexray/crc.py +185 -0
  141. oscura/automotive/flexray/fibex.py +449 -0
  142. oscura/automotive/j1939/__init__.py +45 -8
  143. oscura/automotive/j1939/analyzer.py +605 -0
  144. oscura/automotive/j1939/spns.py +326 -0
  145. oscura/automotive/j1939/transport.py +306 -0
  146. oscura/automotive/lin/__init__.py +47 -0
  147. oscura/automotive/lin/analyzer.py +612 -0
  148. oscura/automotive/loaders/blf.py +13 -2
  149. oscura/automotive/loaders/csv_can.py +143 -72
  150. oscura/automotive/loaders/dispatcher.py +50 -2
  151. oscura/automotive/loaders/mdf.py +86 -45
  152. oscura/automotive/loaders/pcap.py +111 -61
  153. oscura/automotive/uds/__init__.py +4 -0
  154. oscura/automotive/uds/analyzer.py +725 -0
  155. oscura/automotive/uds/decoder.py +140 -58
  156. oscura/automotive/uds/models.py +7 -1
  157. oscura/automotive/visualization.py +1 -1
  158. oscura/cli/analyze.py +348 -0
  159. oscura/cli/batch.py +142 -122
  160. oscura/cli/benchmark.py +275 -0
  161. oscura/cli/characterize.py +137 -82
  162. oscura/cli/compare.py +224 -131
  163. oscura/cli/completion.py +250 -0
  164. oscura/cli/config_cmd.py +361 -0
  165. oscura/cli/decode.py +164 -87
  166. oscura/cli/export.py +286 -0
  167. oscura/cli/main.py +115 -31
  168. oscura/{onboarding → cli/onboarding}/__init__.py +3 -3
  169. oscura/{onboarding → cli/onboarding}/help.py +80 -58
  170. oscura/{onboarding → cli/onboarding}/tutorials.py +97 -72
  171. oscura/{onboarding → cli/onboarding}/wizard.py +55 -36
  172. oscura/cli/progress.py +147 -0
  173. oscura/cli/shell.py +157 -135
  174. oscura/cli/validate_cmd.py +204 -0
  175. oscura/cli/visualize.py +158 -0
  176. oscura/convenience.py +125 -79
  177. oscura/core/__init__.py +4 -2
  178. oscura/core/backend_selector.py +3 -3
  179. oscura/core/cache.py +126 -15
  180. oscura/core/cancellation.py +1 -1
  181. oscura/{config → core/config}/__init__.py +20 -11
  182. oscura/{config → core/config}/defaults.py +1 -1
  183. oscura/{config → core/config}/loader.py +7 -5
  184. oscura/{config → core/config}/memory.py +5 -5
  185. oscura/{config → core/config}/migration.py +1 -1
  186. oscura/{config → core/config}/pipeline.py +99 -23
  187. oscura/{config → core/config}/preferences.py +1 -1
  188. oscura/{config → core/config}/protocol.py +3 -3
  189. oscura/{config → core/config}/schema.py +426 -272
  190. oscura/{config → core/config}/settings.py +1 -1
  191. oscura/{config → core/config}/thresholds.py +195 -153
  192. oscura/core/correlation.py +5 -6
  193. oscura/core/cross_domain.py +0 -2
  194. oscura/core/debug.py +9 -5
  195. oscura/{extensibility → core/extensibility}/docs.py +158 -70
  196. oscura/{extensibility → core/extensibility}/extensions.py +160 -76
  197. oscura/{extensibility → core/extensibility}/logging.py +1 -1
  198. oscura/{extensibility → core/extensibility}/measurements.py +1 -1
  199. oscura/{extensibility → core/extensibility}/plugins.py +1 -1
  200. oscura/{extensibility → core/extensibility}/templates.py +73 -3
  201. oscura/{extensibility → core/extensibility}/validation.py +1 -1
  202. oscura/core/gpu_backend.py +11 -7
  203. oscura/core/log_query.py +101 -11
  204. oscura/core/logging.py +126 -54
  205. oscura/core/logging_advanced.py +5 -5
  206. oscura/core/memory_limits.py +108 -70
  207. oscura/core/memory_monitor.py +2 -2
  208. oscura/core/memory_progress.py +7 -7
  209. oscura/core/memory_warnings.py +1 -1
  210. oscura/core/numba_backend.py +13 -13
  211. oscura/{plugins → core/plugins}/__init__.py +9 -9
  212. oscura/{plugins → core/plugins}/base.py +7 -7
  213. oscura/{plugins → core/plugins}/cli.py +3 -3
  214. oscura/{plugins → core/plugins}/discovery.py +186 -106
  215. oscura/{plugins → core/plugins}/lifecycle.py +1 -1
  216. oscura/{plugins → core/plugins}/manager.py +7 -7
  217. oscura/{plugins → core/plugins}/registry.py +3 -3
  218. oscura/{plugins → core/plugins}/versioning.py +1 -1
  219. oscura/core/progress.py +16 -1
  220. oscura/core/provenance.py +8 -2
  221. oscura/{schemas → core/schemas}/__init__.py +2 -2
  222. oscura/core/schemas/bus_configuration.json +322 -0
  223. oscura/core/schemas/device_mapping.json +182 -0
  224. oscura/core/schemas/packet_format.json +418 -0
  225. oscura/core/schemas/protocol_definition.json +363 -0
  226. oscura/core/types.py +4 -0
  227. oscura/core/uncertainty.py +3 -3
  228. oscura/correlation/__init__.py +52 -0
  229. oscura/correlation/multi_protocol.py +811 -0
  230. oscura/discovery/auto_decoder.py +117 -35
  231. oscura/discovery/comparison.py +191 -86
  232. oscura/discovery/quality_validator.py +155 -68
  233. oscura/discovery/signal_detector.py +196 -79
  234. oscura/export/__init__.py +18 -20
  235. oscura/export/kaitai_struct.py +513 -0
  236. oscura/export/scapy_layer.py +801 -0
  237. oscura/export/wireshark/README.md +15 -15
  238. oscura/export/wireshark/generator.py +1 -1
  239. oscura/export/wireshark/templates/dissector.lua.j2 +2 -2
  240. oscura/export/wireshark_dissector.py +746 -0
  241. oscura/guidance/wizard.py +207 -111
  242. oscura/hardware/__init__.py +19 -0
  243. oscura/{acquisition → hardware/acquisition}/__init__.py +4 -4
  244. oscura/{acquisition → hardware/acquisition}/file.py +2 -2
  245. oscura/{acquisition → hardware/acquisition}/hardware.py +7 -7
  246. oscura/{acquisition → hardware/acquisition}/saleae.py +15 -12
  247. oscura/{acquisition → hardware/acquisition}/socketcan.py +1 -1
  248. oscura/{acquisition → hardware/acquisition}/streaming.py +2 -2
  249. oscura/{acquisition → hardware/acquisition}/synthetic.py +3 -3
  250. oscura/{acquisition → hardware/acquisition}/visa.py +33 -11
  251. oscura/hardware/firmware/__init__.py +29 -0
  252. oscura/hardware/firmware/pattern_recognition.py +874 -0
  253. oscura/hardware/hal_detector.py +736 -0
  254. oscura/hardware/security/__init__.py +37 -0
  255. oscura/hardware/security/side_channel_detector.py +1126 -0
  256. oscura/inference/__init__.py +4 -0
  257. oscura/inference/active_learning/README.md +7 -7
  258. oscura/inference/active_learning/observation_table.py +4 -1
  259. oscura/inference/alignment.py +216 -123
  260. oscura/inference/bayesian.py +113 -33
  261. oscura/inference/crc_reverse.py +101 -55
  262. oscura/inference/logic.py +6 -2
  263. oscura/inference/message_format.py +342 -183
  264. oscura/inference/protocol.py +95 -44
  265. oscura/inference/protocol_dsl.py +180 -82
  266. oscura/inference/signal_intelligence.py +1439 -706
  267. oscura/inference/spectral.py +99 -57
  268. oscura/inference/state_machine.py +810 -158
  269. oscura/inference/stream.py +270 -110
  270. oscura/iot/__init__.py +34 -0
  271. oscura/iot/coap/__init__.py +32 -0
  272. oscura/iot/coap/analyzer.py +668 -0
  273. oscura/iot/coap/options.py +212 -0
  274. oscura/iot/lorawan/__init__.py +21 -0
  275. oscura/iot/lorawan/crypto.py +206 -0
  276. oscura/iot/lorawan/decoder.py +801 -0
  277. oscura/iot/lorawan/mac_commands.py +341 -0
  278. oscura/iot/mqtt/__init__.py +27 -0
  279. oscura/iot/mqtt/analyzer.py +999 -0
  280. oscura/iot/mqtt/properties.py +315 -0
  281. oscura/iot/zigbee/__init__.py +31 -0
  282. oscura/iot/zigbee/analyzer.py +615 -0
  283. oscura/iot/zigbee/security.py +153 -0
  284. oscura/iot/zigbee/zcl.py +349 -0
  285. oscura/jupyter/display.py +125 -45
  286. oscura/{exploratory → jupyter/exploratory}/__init__.py +8 -8
  287. oscura/{exploratory → jupyter/exploratory}/error_recovery.py +298 -141
  288. oscura/jupyter/exploratory/fuzzy.py +746 -0
  289. oscura/{exploratory → jupyter/exploratory}/fuzzy_advanced.py +258 -100
  290. oscura/{exploratory → jupyter/exploratory}/legacy.py +464 -242
  291. oscura/{exploratory → jupyter/exploratory}/parse.py +167 -145
  292. oscura/{exploratory → jupyter/exploratory}/recovery.py +119 -87
  293. oscura/jupyter/exploratory/sync.py +612 -0
  294. oscura/{exploratory → jupyter/exploratory}/unknown.py +299 -176
  295. oscura/jupyter/magic.py +4 -4
  296. oscura/{ui → jupyter/ui}/__init__.py +2 -2
  297. oscura/{ui → jupyter/ui}/formatters.py +3 -3
  298. oscura/{ui → jupyter/ui}/progressive_display.py +153 -82
  299. oscura/loaders/__init__.py +171 -63
  300. oscura/loaders/binary.py +88 -1
  301. oscura/loaders/chipwhisperer.py +153 -137
  302. oscura/loaders/configurable.py +208 -86
  303. oscura/loaders/csv_loader.py +458 -215
  304. oscura/loaders/hdf5_loader.py +278 -119
  305. oscura/loaders/lazy.py +87 -54
  306. oscura/loaders/mmap_loader.py +1 -1
  307. oscura/loaders/numpy_loader.py +253 -116
  308. oscura/loaders/pcap.py +226 -151
  309. oscura/loaders/rigol.py +110 -49
  310. oscura/loaders/sigrok.py +201 -78
  311. oscura/loaders/tdms.py +81 -58
  312. oscura/loaders/tektronix.py +291 -174
  313. oscura/loaders/touchstone.py +182 -87
  314. oscura/loaders/vcd.py +215 -117
  315. oscura/loaders/wav.py +155 -68
  316. oscura/reporting/__init__.py +9 -7
  317. oscura/reporting/analyze.py +352 -146
  318. oscura/reporting/argument_preparer.py +69 -14
  319. oscura/reporting/auto_report.py +97 -61
  320. oscura/reporting/batch.py +131 -58
  321. oscura/reporting/chart_selection.py +57 -45
  322. oscura/reporting/comparison.py +63 -17
  323. oscura/reporting/content/executive.py +76 -24
  324. oscura/reporting/core_formats/multi_format.py +11 -8
  325. oscura/reporting/engine.py +312 -158
  326. oscura/reporting/enhanced_reports.py +949 -0
  327. oscura/reporting/export.py +86 -43
  328. oscura/reporting/formatting/numbers.py +69 -42
  329. oscura/reporting/html.py +139 -58
  330. oscura/reporting/index.py +137 -65
  331. oscura/reporting/output.py +158 -67
  332. oscura/reporting/pdf.py +67 -102
  333. oscura/reporting/plots.py +191 -112
  334. oscura/reporting/sections.py +88 -47
  335. oscura/reporting/standards.py +104 -61
  336. oscura/reporting/summary_generator.py +75 -55
  337. oscura/reporting/tables.py +138 -54
  338. oscura/reporting/templates/enhanced/protocol_re.html +525 -0
  339. oscura/reporting/templates/index.md +13 -13
  340. oscura/sessions/__init__.py +14 -23
  341. oscura/sessions/base.py +3 -3
  342. oscura/sessions/blackbox.py +106 -10
  343. oscura/sessions/generic.py +2 -2
  344. oscura/sessions/legacy.py +783 -0
  345. oscura/side_channel/__init__.py +63 -0
  346. oscura/side_channel/dpa.py +1025 -0
  347. oscura/utils/__init__.py +15 -1
  348. oscura/utils/autodetect.py +1 -5
  349. oscura/utils/bitwise.py +118 -0
  350. oscura/{builders → utils/builders}/__init__.py +1 -1
  351. oscura/{comparison → utils/comparison}/__init__.py +6 -6
  352. oscura/{comparison → utils/comparison}/compare.py +202 -101
  353. oscura/{comparison → utils/comparison}/golden.py +83 -63
  354. oscura/{comparison → utils/comparison}/limits.py +313 -89
  355. oscura/{comparison → utils/comparison}/mask.py +151 -45
  356. oscura/{comparison → utils/comparison}/trace_diff.py +1 -1
  357. oscura/{comparison → utils/comparison}/visualization.py +147 -89
  358. oscura/{component → utils/component}/__init__.py +3 -3
  359. oscura/{component → utils/component}/impedance.py +122 -58
  360. oscura/{component → utils/component}/reactive.py +165 -168
  361. oscura/{component → utils/component}/transmission_line.py +3 -3
  362. oscura/{filtering → utils/filtering}/__init__.py +6 -6
  363. oscura/{filtering → utils/filtering}/base.py +1 -1
  364. oscura/{filtering → utils/filtering}/convenience.py +2 -2
  365. oscura/{filtering → utils/filtering}/design.py +169 -93
  366. oscura/{filtering → utils/filtering}/filters.py +2 -2
  367. oscura/{filtering → utils/filtering}/introspection.py +2 -2
  368. oscura/utils/geometry.py +31 -0
  369. oscura/utils/imports.py +184 -0
  370. oscura/utils/lazy.py +1 -1
  371. oscura/{math → utils/math}/__init__.py +2 -2
  372. oscura/{math → utils/math}/arithmetic.py +114 -48
  373. oscura/{math → utils/math}/interpolation.py +139 -106
  374. oscura/utils/memory.py +129 -66
  375. oscura/utils/memory_advanced.py +92 -9
  376. oscura/utils/memory_extensions.py +10 -8
  377. oscura/{optimization → utils/optimization}/__init__.py +1 -1
  378. oscura/{optimization → utils/optimization}/search.py +2 -2
  379. oscura/utils/performance/__init__.py +58 -0
  380. oscura/utils/performance/caching.py +889 -0
  381. oscura/utils/performance/lsh_clustering.py +333 -0
  382. oscura/utils/performance/memory_optimizer.py +699 -0
  383. oscura/utils/performance/optimizations.py +675 -0
  384. oscura/utils/performance/parallel.py +654 -0
  385. oscura/utils/performance/profiling.py +661 -0
  386. oscura/{pipeline → utils/pipeline}/base.py +1 -1
  387. oscura/{pipeline → utils/pipeline}/composition.py +11 -3
  388. oscura/{pipeline → utils/pipeline}/parallel.py +3 -2
  389. oscura/{pipeline → utils/pipeline}/pipeline.py +1 -1
  390. oscura/{pipeline → utils/pipeline}/reverse_engineering.py +412 -221
  391. oscura/{search → utils/search}/__init__.py +3 -3
  392. oscura/{search → utils/search}/anomaly.py +188 -58
  393. oscura/utils/search/context.py +294 -0
  394. oscura/{search → utils/search}/pattern.py +138 -10
  395. oscura/utils/serial.py +51 -0
  396. oscura/utils/storage/__init__.py +61 -0
  397. oscura/utils/storage/database.py +1166 -0
  398. oscura/{streaming → utils/streaming}/chunked.py +302 -143
  399. oscura/{streaming → utils/streaming}/progressive.py +1 -1
  400. oscura/{streaming → utils/streaming}/realtime.py +3 -2
  401. oscura/{triggering → utils/triggering}/__init__.py +6 -6
  402. oscura/{triggering → utils/triggering}/base.py +6 -6
  403. oscura/{triggering → utils/triggering}/edge.py +2 -2
  404. oscura/{triggering → utils/triggering}/pattern.py +2 -2
  405. oscura/{triggering → utils/triggering}/pulse.py +115 -74
  406. oscura/{triggering → utils/triggering}/window.py +2 -2
  407. oscura/utils/validation.py +32 -0
  408. oscura/validation/__init__.py +121 -0
  409. oscura/{compliance → validation/compliance}/__init__.py +5 -5
  410. oscura/{compliance → validation/compliance}/advanced.py +5 -5
  411. oscura/{compliance → validation/compliance}/masks.py +1 -1
  412. oscura/{compliance → validation/compliance}/reporting.py +127 -53
  413. oscura/{compliance → validation/compliance}/testing.py +114 -52
  414. oscura/validation/compliance_tests.py +915 -0
  415. oscura/validation/fuzzer.py +990 -0
  416. oscura/validation/grammar_tests.py +596 -0
  417. oscura/validation/grammar_validator.py +904 -0
  418. oscura/validation/hil_testing.py +977 -0
  419. oscura/{quality → validation/quality}/__init__.py +4 -4
  420. oscura/{quality → validation/quality}/ensemble.py +251 -171
  421. oscura/{quality → validation/quality}/explainer.py +3 -3
  422. oscura/{quality → validation/quality}/scoring.py +1 -1
  423. oscura/{quality → validation/quality}/warnings.py +4 -4
  424. oscura/validation/regression_suite.py +808 -0
  425. oscura/validation/replay.py +788 -0
  426. oscura/{testing → validation/testing}/__init__.py +2 -2
  427. oscura/{testing → validation/testing}/synthetic.py +5 -5
  428. oscura/visualization/__init__.py +9 -0
  429. oscura/visualization/accessibility.py +1 -1
  430. oscura/visualization/annotations.py +64 -67
  431. oscura/visualization/colors.py +7 -7
  432. oscura/visualization/digital.py +180 -81
  433. oscura/visualization/eye.py +236 -85
  434. oscura/visualization/interactive.py +320 -143
  435. oscura/visualization/jitter.py +587 -247
  436. oscura/visualization/layout.py +169 -134
  437. oscura/visualization/optimization.py +103 -52
  438. oscura/visualization/palettes.py +1 -1
  439. oscura/visualization/power.py +427 -211
  440. oscura/visualization/power_extended.py +626 -297
  441. oscura/visualization/presets.py +2 -0
  442. oscura/visualization/protocols.py +495 -181
  443. oscura/visualization/render.py +79 -63
  444. oscura/visualization/reverse_engineering.py +171 -124
  445. oscura/visualization/signal_integrity.py +460 -279
  446. oscura/visualization/specialized.py +190 -100
  447. oscura/visualization/spectral.py +670 -255
  448. oscura/visualization/thumbnails.py +166 -137
  449. oscura/visualization/waveform.py +150 -63
  450. oscura/workflows/__init__.py +3 -0
  451. oscura/{batch → workflows/batch}/__init__.py +5 -5
  452. oscura/{batch → workflows/batch}/advanced.py +150 -75
  453. oscura/workflows/batch/aggregate.py +531 -0
  454. oscura/workflows/batch/analyze.py +236 -0
  455. oscura/{batch → workflows/batch}/logging.py +2 -2
  456. oscura/{batch → workflows/batch}/metrics.py +1 -1
  457. oscura/workflows/complete_re.py +1144 -0
  458. oscura/workflows/compliance.py +44 -54
  459. oscura/workflows/digital.py +197 -51
  460. oscura/workflows/legacy/__init__.py +12 -0
  461. oscura/{workflow → workflows/legacy}/dag.py +4 -1
  462. oscura/workflows/multi_trace.py +9 -9
  463. oscura/workflows/power.py +42 -62
  464. oscura/workflows/protocol.py +82 -49
  465. oscura/workflows/reverse_engineering.py +351 -150
  466. oscura/workflows/signal_integrity.py +157 -82
  467. oscura-0.6.0.dist-info/METADATA +643 -0
  468. oscura-0.6.0.dist-info/RECORD +590 -0
  469. oscura/analyzers/digital/ic_database.py +0 -498
  470. oscura/analyzers/digital/timing_paths.py +0 -339
  471. oscura/analyzers/digital/vintage.py +0 -377
  472. oscura/analyzers/digital/vintage_result.py +0 -148
  473. oscura/analyzers/protocols/parallel_bus.py +0 -449
  474. oscura/batch/aggregate.py +0 -300
  475. oscura/batch/analyze.py +0 -139
  476. oscura/dsl/__init__.py +0 -73
  477. oscura/exceptions.py +0 -59
  478. oscura/exploratory/fuzzy.py +0 -513
  479. oscura/exploratory/sync.py +0 -384
  480. oscura/export/wavedrom.py +0 -430
  481. oscura/exporters/__init__.py +0 -94
  482. oscura/exporters/csv.py +0 -303
  483. oscura/exporters/exporters.py +0 -44
  484. oscura/exporters/hdf5.py +0 -217
  485. oscura/exporters/html_export.py +0 -701
  486. oscura/exporters/json_export.py +0 -338
  487. oscura/exporters/markdown_export.py +0 -367
  488. oscura/exporters/matlab_export.py +0 -354
  489. oscura/exporters/npz_export.py +0 -219
  490. oscura/exporters/spice_export.py +0 -210
  491. oscura/exporters/vintage_logic_csv.py +0 -247
  492. oscura/reporting/vintage_logic_report.py +0 -523
  493. oscura/search/context.py +0 -149
  494. oscura/session/__init__.py +0 -34
  495. oscura/session/annotations.py +0 -289
  496. oscura/session/history.py +0 -313
  497. oscura/session/session.py +0 -520
  498. oscura/visualization/digital_advanced.py +0 -718
  499. oscura/visualization/figure_manager.py +0 -156
  500. oscura/workflow/__init__.py +0 -13
  501. oscura-0.5.0.dist-info/METADATA +0 -407
  502. oscura-0.5.0.dist-info/RECORD +0 -486
  503. /oscura/core/{config.py → config/legacy.py} +0 -0
  504. /oscura/{extensibility → core/extensibility}/__init__.py +0 -0
  505. /oscura/{extensibility → core/extensibility}/registry.py +0 -0
  506. /oscura/{plugins → core/plugins}/isolation.py +0 -0
  507. /oscura/{builders → utils/builders}/signal_builder.py +0 -0
  508. /oscura/{optimization → utils/optimization}/parallel.py +0 -0
  509. /oscura/{pipeline → utils/pipeline}/__init__.py +0 -0
  510. /oscura/{streaming → utils/streaming}/__init__.py +0 -0
  511. {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/WHEEL +0 -0
  512. {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/entry_points.txt +0 -0
  513. {oscura-0.5.0.dist-info → oscura-0.6.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"]