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,552 @@
1
+ """OPC UA (Unified Architecture) protocol analyzer.
2
+
3
+ This module provides comprehensive OPC UA binary protocol analysis for
4
+ industrial automation and SCADA systems communication. Supports message
5
+ parsing, service decoding, security policy handling, and address space export.
6
+
7
+ Example:
8
+ >>> from oscura.analyzers.protocols.industrial.opcua import OPCUAAnalyzer
9
+ >>> analyzer = OPCUAAnalyzer()
10
+ >>> # Parse Hello message
11
+ >>> hello = bytes([0x48, 0x45, 0x4C, 0x46, 0x1C, 0x00, 0x00, 0x00, ...])
12
+ >>> msg = analyzer.parse_message(hello, timestamp=0.0)
13
+ >>> print(f"{msg.message_type}: {msg.decoded_service}")
14
+ >>> # Export discovered address space
15
+ >>> analyzer.export_address_space(Path("opcua_nodes.json"))
16
+
17
+ References:
18
+ OPC UA Part 6: Mappings (Binary Protocol)
19
+ https://reference.opcfoundation.org/Core/Part6/v105/docs/
20
+
21
+ OPC UA Part 4: Services
22
+ https://reference.opcfoundation.org/Core/Part4/v105/docs/
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.analyzers.protocols.industrial.opcua.datatypes import parse_string
33
+ from oscura.analyzers.protocols.industrial.opcua.services import SERVICE_PARSERS
34
+
35
+
36
+ @dataclass
37
+ class OPCUAMessage:
38
+ """OPC UA message representation.
39
+
40
+ Attributes:
41
+ timestamp: Message timestamp in seconds.
42
+ message_type: Message type ("HEL", "ACK", "OPN", "CLO", "MSG", "ERR").
43
+ is_final: True if final chunk ('F' flag).
44
+ chunk_type: Chunk type character ('F', 'C', 'A') or None.
45
+ secure_channel_id: Secure channel identifier.
46
+ security_token_id: Security token ID (for secure messages).
47
+ sequence_number: Message sequence number (for MSG chunks).
48
+ request_id: Request identifier (for MSG chunks).
49
+ service_id: Service type identifier (NodeId).
50
+ service_name: Human-readable service name.
51
+ payload: Raw payload bytes.
52
+ decoded_service: Parsed service-specific data.
53
+ """
54
+
55
+ timestamp: float
56
+ message_type: str # "HEL", "ACK", "OPN", "CLO", "MSG", "ERR"
57
+ is_final: bool # 'F' flag
58
+ chunk_type: str | None = None # For chunked messages
59
+ secure_channel_id: int = 0
60
+ security_token_id: int | None = None
61
+ sequence_number: int | None = None
62
+ request_id: int | None = None
63
+ service_id: int | None = None
64
+ service_name: str | None = None
65
+ payload: bytes = b""
66
+ decoded_service: dict[str, Any] = field(default_factory=dict)
67
+
68
+
69
+ @dataclass
70
+ class OPCUANode:
71
+ """OPC UA node in address space.
72
+
73
+ Represents a node in the OPC UA information model address space.
74
+
75
+ Attributes:
76
+ node_id: Node identifier string (e.g., "ns=2;i=1001").
77
+ node_class: Node class ("Object", "Variable", "Method", etc.).
78
+ browse_name: Qualified name for browsing.
79
+ display_name: Localized display name.
80
+ value: Current value (for Variable nodes).
81
+ data_type: Data type identifier (for Variable nodes).
82
+ children: List of child node IDs.
83
+ """
84
+
85
+ node_id: str # e.g., "ns=2;i=1001"
86
+ node_class: str # "Object", "Variable", "Method", etc.
87
+ browse_name: str | None = None
88
+ display_name: str | None = None
89
+ value: Any = None
90
+ data_type: str | None = None
91
+ children: list[str] = field(default_factory=list)
92
+
93
+
94
+ class OPCUAAnalyzer:
95
+ """OPC UA protocol analyzer for binary protocol.
96
+
97
+ Provides comprehensive OPC UA protocol analysis including message parsing,
98
+ service decoding, security handling, and address space discovery.
99
+
100
+ Attributes:
101
+ messages: List of parsed OPC UA messages.
102
+ nodes: Dictionary of discovered nodes by node ID.
103
+ security_mode: Current security mode ("None", "Sign", "SignAndEncrypt").
104
+
105
+ Example:
106
+ >>> analyzer = OPCUAAnalyzer()
107
+ >>> # Parse Hello message
108
+ >>> hello_msg = bytes([0x48, 0x45, 0x4C, 0x46, ...]) # HEL + F
109
+ >>> msg = analyzer.parse_message(hello_msg)
110
+ >>> assert msg.message_type == "HEL"
111
+ >>> # Parse MSG chunk
112
+ >>> msg_chunk = bytes([0x4D, 0x53, 0x47, 0x46, ...]) # MSG + F
113
+ >>> msg = analyzer.parse_message(msg_chunk)
114
+ >>> print(f"Service: {msg.service_name}")
115
+ """
116
+
117
+ # Message type constants (3-byte identifiers)
118
+ MESSAGE_TYPES: ClassVar[dict[int, str]] = {
119
+ 0x48454C: "HEL", # Hello (H E L)
120
+ 0x41434B: "ACK", # Acknowledge (A C K)
121
+ 0x4F504E: "OPN", # Open Secure Channel (O P N)
122
+ 0x434C4F: "CLO", # Close Secure Channel (C L O)
123
+ 0x4D5347: "MSG", # Message (M S G)
124
+ 0x455252: "ERR", # Error (E R R)
125
+ }
126
+
127
+ # Service IDs (subset of most common services)
128
+ SERVICE_IDS: ClassVar[dict[int, str]] = {
129
+ 421: "ReadRequest",
130
+ 424: "ReadResponse",
131
+ 673: "WriteRequest",
132
+ 676: "WriteResponse",
133
+ 527: "BrowseRequest",
134
+ 530: "BrowseResponse",
135
+ 631: "CreateSubscriptionRequest",
136
+ 634: "CreateSubscriptionResponse",
137
+ 826: "PublishRequest",
138
+ 829: "PublishResponse",
139
+ 445: "GetEndpointsRequest",
140
+ 448: "GetEndpointsResponse",
141
+ 461: "OpenSecureChannelRequest",
142
+ 464: "OpenSecureChannelResponse",
143
+ 465: "CloseSecureChannelRequest",
144
+ 468: "CloseSecureChannelResponse",
145
+ }
146
+
147
+ # Node class enumeration
148
+ NODE_CLASSES: ClassVar[dict[int, str]] = {
149
+ 1: "Object",
150
+ 2: "Variable",
151
+ 4: "Method",
152
+ 8: "ObjectType",
153
+ 16: "VariableType",
154
+ 32: "ReferenceType",
155
+ 64: "DataType",
156
+ 128: "View",
157
+ }
158
+
159
+ def __init__(self) -> None:
160
+ """Initialize OPC UA analyzer."""
161
+ self.messages: list[OPCUAMessage] = []
162
+ self.nodes: dict[str, OPCUANode] = {}
163
+ self.security_mode: str = "None"
164
+
165
+ def parse_message(self, data: bytes, timestamp: float = 0.0) -> OPCUAMessage:
166
+ """Parse OPC UA binary protocol message.
167
+
168
+ Message Header (8 bytes):
169
+ - MessageType (3 bytes) - "HEL", "ACK", "OPN", "MSG", etc.
170
+ - ChunkType (1 byte) - 'F' (Final), 'C' (Continue), 'A' (Abort)
171
+ - MessageSize (4 bytes, little-endian) - Total message size
172
+
173
+ Args:
174
+ data: Complete message bytes including header.
175
+ timestamp: Message timestamp in seconds.
176
+
177
+ Returns:
178
+ Parsed OPC UA message.
179
+
180
+ Raises:
181
+ ValueError: If message is invalid.
182
+
183
+ Example:
184
+ >>> analyzer = OPCUAAnalyzer()
185
+ >>> # Hello message
186
+ >>> hello = bytes([0x48, 0x45, 0x4C, 0x46, 0x1C, 0x00, 0x00, 0x00])
187
+ >>> msg = analyzer.parse_message(hello)
188
+ >>> assert msg.message_type == "HEL"
189
+ >>> assert msg.is_final is True
190
+ """
191
+ if len(data) < 8:
192
+ raise ValueError(f"OPC UA message too short: {len(data)} bytes (minimum 8)")
193
+
194
+ # Parse common header
195
+ header = self._parse_header(data)
196
+
197
+ msg_type = header["message_type"]
198
+ chunk_type = header["chunk_type"]
199
+ is_final = chunk_type == "F"
200
+
201
+ # Parse type-specific payload
202
+ decoded: dict[str, Any] = {}
203
+ payload = data[8:]
204
+
205
+ if msg_type == "HEL":
206
+ decoded = self._parse_hello(payload)
207
+ elif msg_type == "ACK":
208
+ decoded = self._parse_acknowledge(payload)
209
+ elif msg_type == "OPN":
210
+ decoded = self._parse_open_secure_channel(payload)
211
+ elif msg_type == "CLO":
212
+ decoded = self._parse_open_secure_channel(payload) # Same format as OPN
213
+ elif msg_type == "MSG":
214
+ decoded = self._parse_message_chunk(payload)
215
+ elif msg_type == "ERR":
216
+ decoded = self._parse_error(payload)
217
+
218
+ message = OPCUAMessage(
219
+ timestamp=timestamp,
220
+ message_type=msg_type,
221
+ is_final=is_final,
222
+ chunk_type=chunk_type,
223
+ secure_channel_id=decoded.get("secure_channel_id", 0),
224
+ security_token_id=decoded.get("security_token_id"),
225
+ sequence_number=decoded.get("sequence_number"),
226
+ request_id=decoded.get("request_id"),
227
+ service_id=decoded.get("service_id"),
228
+ service_name=self.SERVICE_IDS.get(decoded.get("service_id", 0)),
229
+ payload=payload,
230
+ decoded_service=decoded,
231
+ )
232
+
233
+ self.messages.append(message)
234
+ return message
235
+
236
+ def _parse_header(self, data: bytes) -> dict[str, Any]:
237
+ """Parse message header (8 bytes).
238
+
239
+ Header Format:
240
+ - MessageType (3 bytes) - ASCII characters
241
+ - ChunkType (1 byte) - 'F', 'C', or 'A'
242
+ - MessageSize (4 bytes, little-endian)
243
+
244
+ Args:
245
+ data: Message data starting with header.
246
+
247
+ Returns:
248
+ Dictionary with header fields.
249
+
250
+ Raises:
251
+ ValueError: If header is invalid.
252
+ """
253
+ # Parse message type (3 bytes)
254
+ msg_type_bytes = data[0:3]
255
+ msg_type_val = (msg_type_bytes[0] << 16) | (msg_type_bytes[1] << 8) | msg_type_bytes[2]
256
+ msg_type = self.MESSAGE_TYPES.get(msg_type_val)
257
+
258
+ if msg_type is None:
259
+ # Try to decode as ASCII for better error messages
260
+ try:
261
+ msg_type_str = msg_type_bytes.decode("ascii", errors="ignore")
262
+ raise ValueError(
263
+ f"Unknown OPC UA message type: {msg_type_str} (0x{msg_type_val:06X})"
264
+ )
265
+ except UnicodeDecodeError as exc:
266
+ raise ValueError(f"Invalid OPC UA message type: 0x{msg_type_val:06X}") from exc
267
+
268
+ # Parse chunk type
269
+ chunk_type = chr(data[3])
270
+ if chunk_type not in ("F", "C", "A"):
271
+ raise ValueError(f"Invalid chunk type: {chunk_type} (expected F/C/A)")
272
+
273
+ # Parse message size
274
+ message_size = int.from_bytes(data[4:8], "little")
275
+
276
+ if message_size < 8:
277
+ raise ValueError(f"Invalid message size: {message_size} (minimum 8)")
278
+
279
+ if message_size != len(data):
280
+ raise ValueError(f"Message size mismatch: header={message_size}, actual={len(data)}")
281
+
282
+ return {
283
+ "message_type": msg_type,
284
+ "chunk_type": chunk_type,
285
+ "message_size": message_size,
286
+ }
287
+
288
+ def _parse_hello(self, data: bytes) -> dict[str, Any]:
289
+ """Parse Hello message payload.
290
+
291
+ Hello Message Format (after 8-byte header):
292
+ - ProtocolVersion (4 bytes, little-endian)
293
+ - ReceiveBufferSize (4 bytes, little-endian)
294
+ - SendBufferSize (4 bytes, little-endian)
295
+ - MaxMessageSize (4 bytes, little-endian)
296
+ - MaxChunkCount (4 bytes, little-endian)
297
+ - EndpointUrl (String, length-prefixed UTF-8)
298
+
299
+ Args:
300
+ data: Hello payload (without header).
301
+
302
+ Returns:
303
+ Parsed Hello message data.
304
+
305
+ Raises:
306
+ ValueError: If payload is invalid.
307
+
308
+ Example:
309
+ >>> analyzer = OPCUAAnalyzer()
310
+ >>> payload = bytes([0x00, 0x00, 0x00, 0x00, ...]) # Protocol version 0
311
+ >>> hello = analyzer._parse_hello(payload)
312
+ >>> assert 'protocol_version' in hello
313
+ """
314
+ if len(data) < 20:
315
+ raise ValueError(f"Hello message too short: {len(data)} bytes (minimum 20)")
316
+
317
+ protocol_version = int.from_bytes(data[0:4], "little")
318
+ receive_buffer_size = int.from_bytes(data[4:8], "little")
319
+ send_buffer_size = int.from_bytes(data[8:12], "little")
320
+ max_message_size = int.from_bytes(data[12:16], "little")
321
+ max_chunk_count = int.from_bytes(data[16:20], "little")
322
+
323
+ # Parse endpoint URL (length-prefixed string)
324
+ endpoint_url = None
325
+ if len(data) >= 24:
326
+ url_str, _ = parse_string(data, 20)
327
+ endpoint_url = url_str
328
+
329
+ return {
330
+ "protocol_version": protocol_version,
331
+ "receive_buffer_size": receive_buffer_size,
332
+ "send_buffer_size": send_buffer_size,
333
+ "max_message_size": max_message_size,
334
+ "max_chunk_count": max_chunk_count,
335
+ "endpoint_url": endpoint_url,
336
+ }
337
+
338
+ def _parse_acknowledge(self, data: bytes) -> dict[str, Any]:
339
+ """Parse Acknowledge message payload.
340
+
341
+ Acknowledge Message Format:
342
+ - ProtocolVersion (4 bytes)
343
+ - ReceiveBufferSize (4 bytes)
344
+ - SendBufferSize (4 bytes)
345
+ - MaxMessageSize (4 bytes)
346
+ - MaxChunkCount (4 bytes)
347
+
348
+ Args:
349
+ data: Acknowledge payload.
350
+
351
+ Returns:
352
+ Parsed Acknowledge message data.
353
+ """
354
+ if len(data) < 20:
355
+ raise ValueError(f"Acknowledge message too short: {len(data)} bytes (minimum 20)")
356
+
357
+ return {
358
+ "protocol_version": int.from_bytes(data[0:4], "little"),
359
+ "receive_buffer_size": int.from_bytes(data[4:8], "little"),
360
+ "send_buffer_size": int.from_bytes(data[8:12], "little"),
361
+ "max_message_size": int.from_bytes(data[12:16], "little"),
362
+ "max_chunk_count": int.from_bytes(data[16:20], "little"),
363
+ }
364
+
365
+ def _parse_open_secure_channel(self, data: bytes) -> dict[str, Any]:
366
+ """Parse Open Secure Channel message payload.
367
+
368
+ OpenSecureChannel Format:
369
+ - SecureChannelId (4 bytes)
370
+ - SecurityPolicyUri (String)
371
+ - ... (complex, varies by security mode)
372
+
373
+ Args:
374
+ data: Open Secure Channel payload.
375
+
376
+ Returns:
377
+ Parsed Open Secure Channel data.
378
+ """
379
+ if len(data) < 4:
380
+ raise ValueError("OpenSecureChannel message too short")
381
+
382
+ secure_channel_id = int.from_bytes(data[0:4], "little")
383
+
384
+ # Security policy URI at offset 4
385
+ security_policy_uri = None
386
+ if len(data) > 4:
387
+ policy_uri, _ = parse_string(data, 4)
388
+ security_policy_uri = policy_uri
389
+
390
+ return {
391
+ "secure_channel_id": secure_channel_id,
392
+ "security_policy_uri": security_policy_uri,
393
+ }
394
+
395
+ def _parse_message_chunk(self, data: bytes) -> dict[str, Any]:
396
+ """Parse MSG chunk payload containing service request/response.
397
+
398
+ MSG Chunk Format:
399
+ - SecureChannelId (4 bytes)
400
+ - SecurityTokenId (4 bytes)
401
+ - SequenceNumber (4 bytes)
402
+ - RequestId (4 bytes)
403
+ - Service payload (varies by service type)
404
+
405
+ Args:
406
+ data: MSG chunk payload.
407
+
408
+ Returns:
409
+ Parsed MSG chunk data with service information.
410
+
411
+ Example:
412
+ >>> analyzer = OPCUAAnalyzer()
413
+ >>> # Simplified MSG chunk
414
+ >>> payload = bytes([0x01, 0x00, 0x00, 0x00, ...])
415
+ >>> msg = analyzer._parse_message_chunk(payload)
416
+ >>> assert 'secure_channel_id' in msg
417
+ """
418
+ if len(data) < 16:
419
+ raise ValueError(f"MSG chunk too short: {len(data)} bytes (minimum 16)")
420
+
421
+ secure_channel_id = int.from_bytes(data[0:4], "little")
422
+ security_token_id = int.from_bytes(data[4:8], "little")
423
+ sequence_number = int.from_bytes(data[8:12], "little")
424
+ request_id = int.from_bytes(data[12:16], "little")
425
+
426
+ # Service payload starts at offset 16
427
+ service_payload = data[16:]
428
+
429
+ # Try to decode service type (NodeId at start of service payload)
430
+ service_id = None
431
+ service_data: dict[str, Any] = {}
432
+
433
+ if len(service_payload) >= 4:
434
+ # Service type is encoded as NodeId
435
+ # For simplicity, assume numeric encoding (most common)
436
+ # Real implementation would use parse_node_id
437
+ try:
438
+ # Try to extract service ID (simplified - assumes FourByte encoding)
439
+ if service_payload[0] == 0x01: # FourByte encoding
440
+ service_id = int.from_bytes(service_payload[2:4], "little")
441
+ service_data = self._decode_service(service_id, service_payload[4:])
442
+ except (ValueError, IndexError):
443
+ service_data = {"raw_payload_size": len(service_payload)}
444
+
445
+ return {
446
+ "secure_channel_id": secure_channel_id,
447
+ "security_token_id": security_token_id,
448
+ "sequence_number": sequence_number,
449
+ "request_id": request_id,
450
+ "service_id": service_id,
451
+ "service_data": service_data,
452
+ }
453
+
454
+ def _parse_error(self, data: bytes) -> dict[str, Any]:
455
+ """Parse Error message payload.
456
+
457
+ Error Message Format:
458
+ - Error (4 bytes, StatusCode)
459
+ - Reason (String)
460
+
461
+ Args:
462
+ data: Error payload.
463
+
464
+ Returns:
465
+ Parsed error data.
466
+ """
467
+ if len(data) < 4:
468
+ raise ValueError("Error message too short")
469
+
470
+ error_code = int.from_bytes(data[0:4], "little")
471
+
472
+ # Parse reason string
473
+ reason = None
474
+ if len(data) > 4:
475
+ reason_str, _ = parse_string(data, 4)
476
+ reason = reason_str
477
+
478
+ return {
479
+ "error_code": error_code,
480
+ "reason": reason,
481
+ }
482
+
483
+ def _decode_service(self, service_id: int, payload: bytes) -> dict[str, Any]:
484
+ """Decode service-specific payload.
485
+
486
+ Args:
487
+ service_id: Service type identifier.
488
+ payload: Service payload bytes.
489
+
490
+ Returns:
491
+ Decoded service data.
492
+
493
+ Example:
494
+ >>> analyzer = OPCUAAnalyzer()
495
+ >>> # ReadRequest service
496
+ >>> service_data = analyzer._decode_service(421, b'...')
497
+ >>> assert 'service' in service_data
498
+ """
499
+ # Check if we have a parser for this service
500
+ if service_id in SERVICE_PARSERS:
501
+ parser_request, parser_response = SERVICE_PARSERS[service_id]
502
+ try:
503
+ # Try request parser first (would need context to determine)
504
+ if parser_request is not None:
505
+ result: dict[str, Any] = parser_request(payload)
506
+ return result
507
+ except (ValueError, IndexError):
508
+ pass
509
+
510
+ # Fallback to basic info
511
+ fallback: dict[str, Any] = {
512
+ "service_id": service_id,
513
+ "service_name": self.SERVICE_IDS.get(service_id, "Unknown"),
514
+ "payload_size": len(payload),
515
+ }
516
+ return fallback
517
+
518
+ def export_address_space(self, output_path: Path) -> None:
519
+ """Export discovered address space as JSON.
520
+
521
+ Exports all discovered nodes with their properties to a JSON file.
522
+
523
+ Args:
524
+ output_path: Path to output JSON file.
525
+
526
+ Example:
527
+ >>> analyzer = OPCUAAnalyzer()
528
+ >>> # ... parse messages and discover nodes ...
529
+ >>> analyzer.export_address_space(Path("opcua_nodes.json"))
530
+ """
531
+ export_data = {
532
+ "nodes": [
533
+ {
534
+ "node_id": node.node_id,
535
+ "node_class": node.node_class,
536
+ "browse_name": node.browse_name,
537
+ "display_name": node.display_name,
538
+ "value": str(node.value) if node.value is not None else None,
539
+ "data_type": node.data_type,
540
+ "children": node.children,
541
+ }
542
+ for node in self.nodes.values()
543
+ ],
544
+ "message_count": len(self.messages),
545
+ "security_mode": self.security_mode,
546
+ }
547
+
548
+ with output_path.open("w") as f:
549
+ json.dump(export_data, f, indent=2)
550
+
551
+
552
+ __all__ = ["OPCUAAnalyzer", "OPCUAMessage", "OPCUANode"]