oscura 0.5.1__py3-none-any.whl → 0.7.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (497) hide show
  1. oscura/__init__.py +169 -167
  2. oscura/analyzers/__init__.py +3 -0
  3. oscura/analyzers/classification.py +659 -0
  4. oscura/analyzers/digital/edges.py +325 -65
  5. oscura/analyzers/digital/quality.py +293 -166
  6. oscura/analyzers/digital/timing.py +260 -115
  7. oscura/analyzers/digital/timing_numba.py +334 -0
  8. oscura/analyzers/entropy.py +605 -0
  9. oscura/analyzers/eye/diagram.py +176 -109
  10. oscura/analyzers/eye/metrics.py +5 -5
  11. oscura/analyzers/jitter/__init__.py +6 -4
  12. oscura/analyzers/jitter/ber.py +52 -52
  13. oscura/analyzers/jitter/classification.py +156 -0
  14. oscura/analyzers/jitter/decomposition.py +163 -113
  15. oscura/analyzers/jitter/spectrum.py +80 -64
  16. oscura/analyzers/ml/__init__.py +39 -0
  17. oscura/analyzers/ml/features.py +600 -0
  18. oscura/analyzers/ml/signal_classifier.py +604 -0
  19. oscura/analyzers/packet/daq.py +246 -158
  20. oscura/analyzers/packet/parser.py +12 -1
  21. oscura/analyzers/packet/payload.py +50 -2110
  22. oscura/analyzers/packet/payload_analysis.py +361 -181
  23. oscura/analyzers/packet/payload_patterns.py +133 -70
  24. oscura/analyzers/packet/stream.py +84 -23
  25. oscura/analyzers/patterns/__init__.py +26 -5
  26. oscura/analyzers/patterns/anomaly_detection.py +908 -0
  27. oscura/analyzers/patterns/clustering.py +169 -108
  28. oscura/analyzers/patterns/clustering_optimized.py +227 -0
  29. oscura/analyzers/patterns/discovery.py +1 -1
  30. oscura/analyzers/patterns/matching.py +581 -197
  31. oscura/analyzers/patterns/pattern_mining.py +778 -0
  32. oscura/analyzers/patterns/periodic.py +121 -38
  33. oscura/analyzers/patterns/sequences.py +175 -78
  34. oscura/analyzers/power/conduction.py +1 -1
  35. oscura/analyzers/power/soa.py +6 -6
  36. oscura/analyzers/power/switching.py +250 -110
  37. oscura/analyzers/protocol/__init__.py +17 -1
  38. oscura/analyzers/protocols/base.py +6 -6
  39. oscura/analyzers/protocols/ble/__init__.py +38 -0
  40. oscura/analyzers/protocols/ble/analyzer.py +809 -0
  41. oscura/analyzers/protocols/ble/uuids.py +288 -0
  42. oscura/analyzers/protocols/can.py +257 -127
  43. oscura/analyzers/protocols/can_fd.py +107 -80
  44. oscura/analyzers/protocols/flexray.py +139 -80
  45. oscura/analyzers/protocols/hdlc.py +93 -58
  46. oscura/analyzers/protocols/i2c.py +247 -106
  47. oscura/analyzers/protocols/i2s.py +138 -86
  48. oscura/analyzers/protocols/industrial/__init__.py +40 -0
  49. oscura/analyzers/protocols/industrial/bacnet/__init__.py +33 -0
  50. oscura/analyzers/protocols/industrial/bacnet/analyzer.py +708 -0
  51. oscura/analyzers/protocols/industrial/bacnet/encoding.py +412 -0
  52. oscura/analyzers/protocols/industrial/bacnet/services.py +622 -0
  53. oscura/analyzers/protocols/industrial/ethercat/__init__.py +30 -0
  54. oscura/analyzers/protocols/industrial/ethercat/analyzer.py +474 -0
  55. oscura/analyzers/protocols/industrial/ethercat/mailbox.py +339 -0
  56. oscura/analyzers/protocols/industrial/ethercat/topology.py +166 -0
  57. oscura/analyzers/protocols/industrial/modbus/__init__.py +31 -0
  58. oscura/analyzers/protocols/industrial/modbus/analyzer.py +525 -0
  59. oscura/analyzers/protocols/industrial/modbus/crc.py +79 -0
  60. oscura/analyzers/protocols/industrial/modbus/functions.py +436 -0
  61. oscura/analyzers/protocols/industrial/opcua/__init__.py +21 -0
  62. oscura/analyzers/protocols/industrial/opcua/analyzer.py +552 -0
  63. oscura/analyzers/protocols/industrial/opcua/datatypes.py +446 -0
  64. oscura/analyzers/protocols/industrial/opcua/services.py +264 -0
  65. oscura/analyzers/protocols/industrial/profinet/__init__.py +23 -0
  66. oscura/analyzers/protocols/industrial/profinet/analyzer.py +441 -0
  67. oscura/analyzers/protocols/industrial/profinet/dcp.py +263 -0
  68. oscura/analyzers/protocols/industrial/profinet/ptcp.py +200 -0
  69. oscura/analyzers/protocols/jtag.py +180 -98
  70. oscura/analyzers/protocols/lin.py +219 -114
  71. oscura/analyzers/protocols/manchester.py +4 -4
  72. oscura/analyzers/protocols/onewire.py +253 -149
  73. oscura/analyzers/protocols/parallel_bus/__init__.py +20 -0
  74. oscura/analyzers/protocols/parallel_bus/centronics.py +92 -0
  75. oscura/analyzers/protocols/parallel_bus/gpib.py +137 -0
  76. oscura/analyzers/protocols/spi.py +192 -95
  77. oscura/analyzers/protocols/swd.py +321 -167
  78. oscura/analyzers/protocols/uart.py +267 -125
  79. oscura/analyzers/protocols/usb.py +235 -131
  80. oscura/analyzers/side_channel/power.py +17 -12
  81. oscura/analyzers/signal/__init__.py +15 -0
  82. oscura/analyzers/signal/timing_analysis.py +1086 -0
  83. oscura/analyzers/signal_integrity/__init__.py +4 -1
  84. oscura/analyzers/signal_integrity/sparams.py +2 -19
  85. oscura/analyzers/spectral/chunked.py +129 -60
  86. oscura/analyzers/spectral/chunked_fft.py +300 -94
  87. oscura/analyzers/spectral/chunked_wavelet.py +100 -80
  88. oscura/analyzers/statistical/checksum.py +376 -217
  89. oscura/analyzers/statistical/classification.py +229 -107
  90. oscura/analyzers/statistical/entropy.py +78 -53
  91. oscura/analyzers/statistics/correlation.py +407 -211
  92. oscura/analyzers/statistics/outliers.py +2 -2
  93. oscura/analyzers/statistics/streaming.py +30 -5
  94. oscura/analyzers/validation.py +216 -101
  95. oscura/analyzers/waveform/measurements.py +9 -0
  96. oscura/analyzers/waveform/measurements_with_uncertainty.py +31 -15
  97. oscura/analyzers/waveform/spectral.py +500 -228
  98. oscura/api/__init__.py +31 -5
  99. oscura/api/dsl/__init__.py +582 -0
  100. oscura/{dsl → api/dsl}/commands.py +43 -76
  101. oscura/{dsl → api/dsl}/interpreter.py +26 -51
  102. oscura/{dsl → api/dsl}/parser.py +107 -77
  103. oscura/{dsl → api/dsl}/repl.py +2 -2
  104. oscura/api/dsl.py +1 -1
  105. oscura/{integrations → api/integrations}/__init__.py +1 -1
  106. oscura/{integrations → api/integrations}/llm.py +201 -102
  107. oscura/api/operators.py +3 -3
  108. oscura/api/optimization.py +144 -30
  109. oscura/api/rest_server.py +921 -0
  110. oscura/api/server/__init__.py +17 -0
  111. oscura/api/server/dashboard.py +850 -0
  112. oscura/api/server/static/README.md +34 -0
  113. oscura/api/server/templates/base.html +181 -0
  114. oscura/api/server/templates/export.html +120 -0
  115. oscura/api/server/templates/home.html +284 -0
  116. oscura/api/server/templates/protocols.html +58 -0
  117. oscura/api/server/templates/reports.html +43 -0
  118. oscura/api/server/templates/session_detail.html +89 -0
  119. oscura/api/server/templates/sessions.html +83 -0
  120. oscura/api/server/templates/waveforms.html +73 -0
  121. oscura/automotive/__init__.py +8 -1
  122. oscura/automotive/can/__init__.py +10 -0
  123. oscura/automotive/can/checksum.py +3 -1
  124. oscura/automotive/can/dbc_generator.py +590 -0
  125. oscura/automotive/can/message_wrapper.py +121 -74
  126. oscura/automotive/can/patterns.py +98 -21
  127. oscura/automotive/can/session.py +292 -56
  128. oscura/automotive/can/state_machine.py +6 -3
  129. oscura/automotive/can/stimulus_response.py +97 -75
  130. oscura/automotive/dbc/__init__.py +10 -2
  131. oscura/automotive/dbc/generator.py +84 -56
  132. oscura/automotive/dbc/parser.py +6 -6
  133. oscura/automotive/dtc/data.json +17 -102
  134. oscura/automotive/dtc/database.py +2 -2
  135. oscura/automotive/flexray/__init__.py +31 -0
  136. oscura/automotive/flexray/analyzer.py +504 -0
  137. oscura/automotive/flexray/crc.py +185 -0
  138. oscura/automotive/flexray/fibex.py +449 -0
  139. oscura/automotive/j1939/__init__.py +45 -8
  140. oscura/automotive/j1939/analyzer.py +605 -0
  141. oscura/automotive/j1939/spns.py +326 -0
  142. oscura/automotive/j1939/transport.py +306 -0
  143. oscura/automotive/lin/__init__.py +47 -0
  144. oscura/automotive/lin/analyzer.py +612 -0
  145. oscura/automotive/loaders/blf.py +13 -2
  146. oscura/automotive/loaders/csv_can.py +143 -72
  147. oscura/automotive/loaders/dispatcher.py +50 -2
  148. oscura/automotive/loaders/mdf.py +86 -45
  149. oscura/automotive/loaders/pcap.py +111 -61
  150. oscura/automotive/uds/__init__.py +4 -0
  151. oscura/automotive/uds/analyzer.py +725 -0
  152. oscura/automotive/uds/decoder.py +140 -58
  153. oscura/automotive/uds/models.py +7 -1
  154. oscura/automotive/visualization.py +1 -1
  155. oscura/cli/analyze.py +348 -0
  156. oscura/cli/batch.py +142 -122
  157. oscura/cli/benchmark.py +275 -0
  158. oscura/cli/characterize.py +137 -82
  159. oscura/cli/compare.py +224 -131
  160. oscura/cli/completion.py +250 -0
  161. oscura/cli/config_cmd.py +361 -0
  162. oscura/cli/decode.py +164 -87
  163. oscura/cli/export.py +286 -0
  164. oscura/cli/main.py +115 -31
  165. oscura/{onboarding → cli/onboarding}/__init__.py +3 -3
  166. oscura/{onboarding → cli/onboarding}/help.py +80 -58
  167. oscura/{onboarding → cli/onboarding}/tutorials.py +97 -72
  168. oscura/{onboarding → cli/onboarding}/wizard.py +55 -36
  169. oscura/cli/progress.py +147 -0
  170. oscura/cli/shell.py +157 -135
  171. oscura/cli/validate_cmd.py +204 -0
  172. oscura/cli/visualize.py +158 -0
  173. oscura/convenience.py +125 -79
  174. oscura/core/__init__.py +4 -2
  175. oscura/core/backend_selector.py +3 -3
  176. oscura/core/cache.py +126 -15
  177. oscura/core/cancellation.py +1 -1
  178. oscura/{config → core/config}/__init__.py +20 -11
  179. oscura/{config → core/config}/defaults.py +1 -1
  180. oscura/{config → core/config}/loader.py +7 -5
  181. oscura/{config → core/config}/memory.py +5 -5
  182. oscura/{config → core/config}/migration.py +1 -1
  183. oscura/{config → core/config}/pipeline.py +99 -23
  184. oscura/{config → core/config}/preferences.py +1 -1
  185. oscura/{config → core/config}/protocol.py +3 -3
  186. oscura/{config → core/config}/schema.py +426 -272
  187. oscura/{config → core/config}/settings.py +1 -1
  188. oscura/{config → core/config}/thresholds.py +195 -153
  189. oscura/core/correlation.py +5 -6
  190. oscura/core/cross_domain.py +0 -2
  191. oscura/core/debug.py +9 -5
  192. oscura/{extensibility → core/extensibility}/docs.py +158 -70
  193. oscura/{extensibility → core/extensibility}/extensions.py +160 -76
  194. oscura/{extensibility → core/extensibility}/logging.py +1 -1
  195. oscura/{extensibility → core/extensibility}/measurements.py +1 -1
  196. oscura/{extensibility → core/extensibility}/plugins.py +1 -1
  197. oscura/{extensibility → core/extensibility}/templates.py +73 -3
  198. oscura/{extensibility → core/extensibility}/validation.py +1 -1
  199. oscura/core/gpu_backend.py +11 -7
  200. oscura/core/log_query.py +101 -11
  201. oscura/core/logging.py +126 -54
  202. oscura/core/logging_advanced.py +5 -5
  203. oscura/core/memory_limits.py +108 -70
  204. oscura/core/memory_monitor.py +2 -2
  205. oscura/core/memory_progress.py +7 -7
  206. oscura/core/memory_warnings.py +1 -1
  207. oscura/core/numba_backend.py +13 -13
  208. oscura/{plugins → core/plugins}/__init__.py +9 -9
  209. oscura/{plugins → core/plugins}/base.py +7 -7
  210. oscura/{plugins → core/plugins}/cli.py +3 -3
  211. oscura/{plugins → core/plugins}/discovery.py +186 -106
  212. oscura/{plugins → core/plugins}/lifecycle.py +1 -1
  213. oscura/{plugins → core/plugins}/manager.py +7 -7
  214. oscura/{plugins → core/plugins}/registry.py +3 -3
  215. oscura/{plugins → core/plugins}/versioning.py +1 -1
  216. oscura/core/progress.py +16 -1
  217. oscura/core/provenance.py +8 -2
  218. oscura/{schemas → core/schemas}/__init__.py +2 -2
  219. oscura/{schemas → core/schemas}/device_mapping.json +2 -8
  220. oscura/{schemas → core/schemas}/packet_format.json +4 -24
  221. oscura/{schemas → core/schemas}/protocol_definition.json +2 -12
  222. oscura/core/types.py +4 -0
  223. oscura/core/uncertainty.py +3 -3
  224. oscura/correlation/__init__.py +52 -0
  225. oscura/correlation/multi_protocol.py +811 -0
  226. oscura/discovery/auto_decoder.py +117 -35
  227. oscura/discovery/comparison.py +191 -86
  228. oscura/discovery/quality_validator.py +155 -68
  229. oscura/discovery/signal_detector.py +196 -79
  230. oscura/export/__init__.py +18 -8
  231. oscura/export/kaitai_struct.py +513 -0
  232. oscura/export/scapy_layer.py +801 -0
  233. oscura/export/wireshark/generator.py +1 -1
  234. oscura/export/wireshark/templates/dissector.lua.j2 +2 -2
  235. oscura/export/wireshark_dissector.py +746 -0
  236. oscura/guidance/wizard.py +207 -111
  237. oscura/hardware/__init__.py +19 -0
  238. oscura/{acquisition → hardware/acquisition}/__init__.py +4 -4
  239. oscura/{acquisition → hardware/acquisition}/file.py +2 -2
  240. oscura/{acquisition → hardware/acquisition}/hardware.py +7 -7
  241. oscura/{acquisition → hardware/acquisition}/saleae.py +15 -12
  242. oscura/{acquisition → hardware/acquisition}/socketcan.py +1 -1
  243. oscura/{acquisition → hardware/acquisition}/streaming.py +2 -2
  244. oscura/{acquisition → hardware/acquisition}/synthetic.py +3 -3
  245. oscura/{acquisition → hardware/acquisition}/visa.py +33 -11
  246. oscura/hardware/firmware/__init__.py +29 -0
  247. oscura/hardware/firmware/pattern_recognition.py +874 -0
  248. oscura/hardware/hal_detector.py +736 -0
  249. oscura/hardware/security/__init__.py +37 -0
  250. oscura/hardware/security/side_channel_detector.py +1126 -0
  251. oscura/inference/__init__.py +4 -0
  252. oscura/inference/active_learning/observation_table.py +4 -1
  253. oscura/inference/alignment.py +216 -123
  254. oscura/inference/bayesian.py +113 -33
  255. oscura/inference/crc_reverse.py +101 -55
  256. oscura/inference/logic.py +6 -2
  257. oscura/inference/message_format.py +342 -183
  258. oscura/inference/protocol.py +95 -44
  259. oscura/inference/protocol_dsl.py +180 -82
  260. oscura/inference/signal_intelligence.py +1439 -706
  261. oscura/inference/spectral.py +99 -57
  262. oscura/inference/state_machine.py +810 -158
  263. oscura/inference/stream.py +270 -110
  264. oscura/iot/__init__.py +34 -0
  265. oscura/iot/coap/__init__.py +32 -0
  266. oscura/iot/coap/analyzer.py +668 -0
  267. oscura/iot/coap/options.py +212 -0
  268. oscura/iot/lorawan/__init__.py +21 -0
  269. oscura/iot/lorawan/crypto.py +206 -0
  270. oscura/iot/lorawan/decoder.py +801 -0
  271. oscura/iot/lorawan/mac_commands.py +341 -0
  272. oscura/iot/mqtt/__init__.py +27 -0
  273. oscura/iot/mqtt/analyzer.py +999 -0
  274. oscura/iot/mqtt/properties.py +315 -0
  275. oscura/iot/zigbee/__init__.py +31 -0
  276. oscura/iot/zigbee/analyzer.py +615 -0
  277. oscura/iot/zigbee/security.py +153 -0
  278. oscura/iot/zigbee/zcl.py +349 -0
  279. oscura/jupyter/display.py +125 -45
  280. oscura/{exploratory → jupyter/exploratory}/__init__.py +8 -8
  281. oscura/{exploratory → jupyter/exploratory}/error_recovery.py +298 -141
  282. oscura/jupyter/exploratory/fuzzy.py +746 -0
  283. oscura/{exploratory → jupyter/exploratory}/fuzzy_advanced.py +258 -100
  284. oscura/{exploratory → jupyter/exploratory}/legacy.py +464 -242
  285. oscura/{exploratory → jupyter/exploratory}/parse.py +167 -145
  286. oscura/{exploratory → jupyter/exploratory}/recovery.py +119 -87
  287. oscura/jupyter/exploratory/sync.py +612 -0
  288. oscura/{exploratory → jupyter/exploratory}/unknown.py +299 -176
  289. oscura/jupyter/magic.py +4 -4
  290. oscura/{ui → jupyter/ui}/__init__.py +2 -2
  291. oscura/{ui → jupyter/ui}/formatters.py +3 -3
  292. oscura/{ui → jupyter/ui}/progressive_display.py +153 -82
  293. oscura/loaders/__init__.py +183 -67
  294. oscura/loaders/binary.py +88 -1
  295. oscura/loaders/chipwhisperer.py +153 -137
  296. oscura/loaders/configurable.py +208 -86
  297. oscura/loaders/csv_loader.py +458 -215
  298. oscura/loaders/hdf5_loader.py +278 -119
  299. oscura/loaders/lazy.py +87 -54
  300. oscura/loaders/mmap_loader.py +1 -1
  301. oscura/loaders/numpy_loader.py +253 -116
  302. oscura/loaders/pcap.py +226 -151
  303. oscura/loaders/rigol.py +110 -49
  304. oscura/loaders/sigrok.py +201 -78
  305. oscura/loaders/tdms.py +81 -58
  306. oscura/loaders/tektronix.py +291 -174
  307. oscura/loaders/touchstone.py +182 -87
  308. oscura/loaders/tss.py +456 -0
  309. oscura/loaders/vcd.py +215 -117
  310. oscura/loaders/wav.py +155 -68
  311. oscura/reporting/__init__.py +9 -0
  312. oscura/reporting/analyze.py +352 -146
  313. oscura/reporting/argument_preparer.py +69 -14
  314. oscura/reporting/auto_report.py +97 -61
  315. oscura/reporting/batch.py +131 -58
  316. oscura/reporting/chart_selection.py +57 -45
  317. oscura/reporting/comparison.py +63 -17
  318. oscura/reporting/content/executive.py +76 -24
  319. oscura/reporting/core_formats/multi_format.py +11 -8
  320. oscura/reporting/engine.py +312 -158
  321. oscura/reporting/enhanced_reports.py +949 -0
  322. oscura/reporting/export.py +86 -43
  323. oscura/reporting/formatting/numbers.py +69 -42
  324. oscura/reporting/html.py +139 -58
  325. oscura/reporting/index.py +137 -65
  326. oscura/reporting/output.py +158 -67
  327. oscura/reporting/pdf.py +67 -102
  328. oscura/reporting/plots.py +191 -112
  329. oscura/reporting/sections.py +88 -47
  330. oscura/reporting/standards.py +104 -61
  331. oscura/reporting/summary_generator.py +75 -55
  332. oscura/reporting/tables.py +138 -54
  333. oscura/reporting/templates/enhanced/protocol_re.html +525 -0
  334. oscura/sessions/__init__.py +14 -23
  335. oscura/sessions/base.py +3 -3
  336. oscura/sessions/blackbox.py +106 -10
  337. oscura/sessions/generic.py +2 -2
  338. oscura/sessions/legacy.py +783 -0
  339. oscura/side_channel/__init__.py +63 -0
  340. oscura/side_channel/dpa.py +1025 -0
  341. oscura/utils/__init__.py +15 -1
  342. oscura/utils/bitwise.py +118 -0
  343. oscura/{builders → utils/builders}/__init__.py +1 -1
  344. oscura/{comparison → utils/comparison}/__init__.py +6 -6
  345. oscura/{comparison → utils/comparison}/compare.py +202 -101
  346. oscura/{comparison → utils/comparison}/golden.py +83 -63
  347. oscura/{comparison → utils/comparison}/limits.py +313 -89
  348. oscura/{comparison → utils/comparison}/mask.py +151 -45
  349. oscura/{comparison → utils/comparison}/trace_diff.py +1 -1
  350. oscura/{comparison → utils/comparison}/visualization.py +147 -89
  351. oscura/{component → utils/component}/__init__.py +3 -3
  352. oscura/{component → utils/component}/impedance.py +122 -58
  353. oscura/{component → utils/component}/reactive.py +165 -168
  354. oscura/{component → utils/component}/transmission_line.py +3 -3
  355. oscura/{filtering → utils/filtering}/__init__.py +6 -6
  356. oscura/{filtering → utils/filtering}/base.py +1 -1
  357. oscura/{filtering → utils/filtering}/convenience.py +2 -2
  358. oscura/{filtering → utils/filtering}/design.py +169 -93
  359. oscura/{filtering → utils/filtering}/filters.py +2 -2
  360. oscura/{filtering → utils/filtering}/introspection.py +2 -2
  361. oscura/utils/geometry.py +31 -0
  362. oscura/utils/imports.py +184 -0
  363. oscura/utils/lazy.py +1 -1
  364. oscura/{math → utils/math}/__init__.py +2 -2
  365. oscura/{math → utils/math}/arithmetic.py +114 -48
  366. oscura/{math → utils/math}/interpolation.py +139 -106
  367. oscura/utils/memory.py +129 -66
  368. oscura/utils/memory_advanced.py +92 -9
  369. oscura/utils/memory_extensions.py +10 -8
  370. oscura/{optimization → utils/optimization}/__init__.py +1 -1
  371. oscura/{optimization → utils/optimization}/search.py +2 -2
  372. oscura/utils/performance/__init__.py +58 -0
  373. oscura/utils/performance/caching.py +889 -0
  374. oscura/utils/performance/lsh_clustering.py +333 -0
  375. oscura/utils/performance/memory_optimizer.py +699 -0
  376. oscura/utils/performance/optimizations.py +675 -0
  377. oscura/utils/performance/parallel.py +654 -0
  378. oscura/utils/performance/profiling.py +661 -0
  379. oscura/{pipeline → utils/pipeline}/base.py +1 -1
  380. oscura/{pipeline → utils/pipeline}/composition.py +1 -1
  381. oscura/{pipeline → utils/pipeline}/parallel.py +3 -2
  382. oscura/{pipeline → utils/pipeline}/pipeline.py +1 -1
  383. oscura/{pipeline → utils/pipeline}/reverse_engineering.py +412 -221
  384. oscura/{search → utils/search}/__init__.py +3 -3
  385. oscura/{search → utils/search}/anomaly.py +188 -58
  386. oscura/utils/search/context.py +294 -0
  387. oscura/{search → utils/search}/pattern.py +138 -10
  388. oscura/utils/serial.py +51 -0
  389. oscura/utils/storage/__init__.py +61 -0
  390. oscura/utils/storage/database.py +1166 -0
  391. oscura/{streaming → utils/streaming}/chunked.py +302 -143
  392. oscura/{streaming → utils/streaming}/progressive.py +1 -1
  393. oscura/{streaming → utils/streaming}/realtime.py +3 -2
  394. oscura/{triggering → utils/triggering}/__init__.py +6 -6
  395. oscura/{triggering → utils/triggering}/base.py +6 -6
  396. oscura/{triggering → utils/triggering}/edge.py +2 -2
  397. oscura/{triggering → utils/triggering}/pattern.py +2 -2
  398. oscura/{triggering → utils/triggering}/pulse.py +115 -74
  399. oscura/{triggering → utils/triggering}/window.py +2 -2
  400. oscura/utils/validation.py +32 -0
  401. oscura/validation/__init__.py +121 -0
  402. oscura/{compliance → validation/compliance}/__init__.py +5 -5
  403. oscura/{compliance → validation/compliance}/advanced.py +5 -5
  404. oscura/{compliance → validation/compliance}/masks.py +1 -1
  405. oscura/{compliance → validation/compliance}/reporting.py +127 -53
  406. oscura/{compliance → validation/compliance}/testing.py +114 -52
  407. oscura/validation/compliance_tests.py +915 -0
  408. oscura/validation/fuzzer.py +990 -0
  409. oscura/validation/grammar_tests.py +596 -0
  410. oscura/validation/grammar_validator.py +904 -0
  411. oscura/validation/hil_testing.py +977 -0
  412. oscura/{quality → validation/quality}/__init__.py +4 -4
  413. oscura/{quality → validation/quality}/ensemble.py +251 -171
  414. oscura/{quality → validation/quality}/explainer.py +3 -3
  415. oscura/{quality → validation/quality}/scoring.py +1 -1
  416. oscura/{quality → validation/quality}/warnings.py +4 -4
  417. oscura/validation/regression_suite.py +808 -0
  418. oscura/validation/replay.py +788 -0
  419. oscura/{testing → validation/testing}/__init__.py +2 -2
  420. oscura/{testing → validation/testing}/synthetic.py +5 -5
  421. oscura/visualization/__init__.py +9 -0
  422. oscura/visualization/accessibility.py +1 -1
  423. oscura/visualization/annotations.py +64 -67
  424. oscura/visualization/colors.py +7 -7
  425. oscura/visualization/digital.py +180 -81
  426. oscura/visualization/eye.py +236 -85
  427. oscura/visualization/interactive.py +320 -143
  428. oscura/visualization/jitter.py +587 -247
  429. oscura/visualization/layout.py +169 -134
  430. oscura/visualization/optimization.py +103 -52
  431. oscura/visualization/palettes.py +1 -1
  432. oscura/visualization/power.py +427 -211
  433. oscura/visualization/power_extended.py +626 -297
  434. oscura/visualization/presets.py +2 -0
  435. oscura/visualization/protocols.py +495 -181
  436. oscura/visualization/render.py +79 -63
  437. oscura/visualization/reverse_engineering.py +171 -124
  438. oscura/visualization/signal_integrity.py +460 -279
  439. oscura/visualization/specialized.py +190 -100
  440. oscura/visualization/spectral.py +670 -255
  441. oscura/visualization/thumbnails.py +166 -137
  442. oscura/visualization/waveform.py +150 -63
  443. oscura/workflows/__init__.py +3 -0
  444. oscura/{batch → workflows/batch}/__init__.py +5 -5
  445. oscura/{batch → workflows/batch}/advanced.py +150 -75
  446. oscura/workflows/batch/aggregate.py +531 -0
  447. oscura/workflows/batch/analyze.py +236 -0
  448. oscura/{batch → workflows/batch}/logging.py +2 -2
  449. oscura/{batch → workflows/batch}/metrics.py +1 -1
  450. oscura/workflows/complete_re.py +1144 -0
  451. oscura/workflows/compliance.py +44 -54
  452. oscura/workflows/digital.py +197 -51
  453. oscura/workflows/legacy/__init__.py +12 -0
  454. oscura/{workflow → workflows/legacy}/dag.py +4 -1
  455. oscura/workflows/multi_trace.py +9 -9
  456. oscura/workflows/power.py +42 -62
  457. oscura/workflows/protocol.py +82 -49
  458. oscura/workflows/reverse_engineering.py +351 -150
  459. oscura/workflows/signal_integrity.py +157 -82
  460. oscura-0.7.0.dist-info/METADATA +661 -0
  461. oscura-0.7.0.dist-info/RECORD +591 -0
  462. oscura/batch/aggregate.py +0 -300
  463. oscura/batch/analyze.py +0 -139
  464. oscura/dsl/__init__.py +0 -73
  465. oscura/exceptions.py +0 -59
  466. oscura/exploratory/fuzzy.py +0 -513
  467. oscura/exploratory/sync.py +0 -384
  468. oscura/exporters/__init__.py +0 -94
  469. oscura/exporters/csv.py +0 -303
  470. oscura/exporters/exporters.py +0 -44
  471. oscura/exporters/hdf5.py +0 -217
  472. oscura/exporters/html_export.py +0 -701
  473. oscura/exporters/json_export.py +0 -291
  474. oscura/exporters/markdown_export.py +0 -367
  475. oscura/exporters/matlab_export.py +0 -354
  476. oscura/exporters/npz_export.py +0 -219
  477. oscura/exporters/spice_export.py +0 -210
  478. oscura/search/context.py +0 -149
  479. oscura/session/__init__.py +0 -34
  480. oscura/session/annotations.py +0 -289
  481. oscura/session/history.py +0 -313
  482. oscura/session/session.py +0 -520
  483. oscura/workflow/__init__.py +0 -13
  484. oscura-0.5.1.dist-info/METADATA +0 -583
  485. oscura-0.5.1.dist-info/RECORD +0 -481
  486. /oscura/core/{config.py → config/legacy.py} +0 -0
  487. /oscura/{extensibility → core/extensibility}/__init__.py +0 -0
  488. /oscura/{extensibility → core/extensibility}/registry.py +0 -0
  489. /oscura/{plugins → core/plugins}/isolation.py +0 -0
  490. /oscura/{schemas → core/schemas}/bus_configuration.json +0 -0
  491. /oscura/{builders → utils/builders}/signal_builder.py +0 -0
  492. /oscura/{optimization → utils/optimization}/parallel.py +0 -0
  493. /oscura/{pipeline → utils/pipeline}/__init__.py +0 -0
  494. /oscura/{streaming → utils/streaming}/__init__.py +0 -0
  495. {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/WHEEL +0 -0
  496. {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/entry_points.txt +0 -0
  497. {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/licenses/LICENSE +0 -0
@@ -17,7 +17,7 @@ from collections.abc import Callable, Sequence
17
17
  from dataclasses import dataclass, field
18
18
  from datetime import datetime
19
19
  from pathlib import Path
20
- from typing import Any, ClassVar, Literal
20
+ from typing import Any, ClassVar, Literal, cast
21
21
 
22
22
  logger = logging.getLogger(__name__)
23
23
 
@@ -206,6 +206,111 @@ class REPipeline:
206
206
  self._checkpoint_path: str | None = None
207
207
  self._checkpoint_data: dict[str, Any] = {}
208
208
 
209
+ def _initialize_analysis_context(
210
+ self, data: bytes | Sequence[dict[str, Any]] | Sequence[bytes]
211
+ ) -> dict[str, Any]:
212
+ """Initialize analysis context with empty containers.
213
+
214
+ Args:
215
+ data: Input data to analyze.
216
+
217
+ Returns:
218
+ Initialized context dictionary.
219
+ """
220
+ return {
221
+ "raw_data": data,
222
+ "flows": [],
223
+ "payloads": [],
224
+ "messages": [],
225
+ "patterns": [],
226
+ "clusters": [],
227
+ "schemas": {},
228
+ "protocol_candidates": [],
229
+ "state_machine": None,
230
+ "warnings": [],
231
+ "statistics": {},
232
+ }
233
+
234
+ def _execute_stage(
235
+ self,
236
+ stage_name: str,
237
+ context: dict[str, Any],
238
+ checkpoint: str | None,
239
+ ) -> StageResult:
240
+ """Execute single pipeline stage.
241
+
242
+ Args:
243
+ stage_name: Name of stage to execute.
244
+ context: Analysis context.
245
+ checkpoint: Checkpoint path.
246
+
247
+ Returns:
248
+ StageResult with execution outcome.
249
+ """
250
+ handler = self._stage_handlers.get(stage_name)
251
+ if not handler:
252
+ return StageResult(
253
+ stage_name=stage_name, success=False, duration=0, output=None, error="No handler"
254
+ )
255
+
256
+ try:
257
+ stage_start = time.time()
258
+ output = handler(context)
259
+ stage_duration = time.time() - stage_start
260
+
261
+ if output:
262
+ context.update(output)
263
+
264
+ if checkpoint:
265
+ self._save_checkpoint(checkpoint, stage_name, context)
266
+
267
+ return StageResult(
268
+ stage_name=stage_name,
269
+ success=True,
270
+ duration=stage_duration,
271
+ output=output,
272
+ )
273
+
274
+ except Exception as e:
275
+ warnings_list: list[str] = context.get("warnings", [])
276
+ warnings_list.append(f"Stage {stage_name} failed: {e}")
277
+ context["warnings"] = warnings_list
278
+
279
+ return StageResult(
280
+ stage_name=stage_name,
281
+ success=False,
282
+ duration=0,
283
+ output=None,
284
+ error=str(e),
285
+ )
286
+
287
+ def _execute_all_stages(
288
+ self, context: dict[str, Any], checkpoint: str | None
289
+ ) -> list[StageResult]:
290
+ """Execute all pipeline stages.
291
+
292
+ Args:
293
+ context: Analysis context.
294
+ checkpoint: Checkpoint path.
295
+
296
+ Returns:
297
+ List of stage results.
298
+ """
299
+ stage_results = []
300
+ total_stages = len(self.stages)
301
+
302
+ for i, stage_name in enumerate(self.stages):
303
+ if stage_name in self._checkpoint_data:
304
+ context.update(self._checkpoint_data[stage_name])
305
+ continue
306
+
307
+ self._report_progress(stage_name, (i / total_stages) * 100)
308
+ stage_result = self._execute_stage(stage_name, context, checkpoint)
309
+ stage_results.append(stage_result)
310
+
311
+ self._report_progress("complete", 100)
312
+ return stage_results
313
+
209
314
  def analyze(
210
315
  self,
211
316
  data: bytes | Sequence[dict[str, Any]] | Sequence[bytes],
@@ -229,85 +334,22 @@ class REPipeline:
229
334
  >>> for msg_type in results.message_types:
230
335
  ... print(f"{msg_type.name}: {msg_type.sample_count} samples")
231
336
  """
337
+ # Setup: initialize state and load checkpoint
232
338
  start_time = time.time()
233
339
  self._progress_callback = progress_callback
234
340
  self._checkpoint_path = checkpoint
235
341
  self._checkpoint_data = {}
236
342
 
237
- # Load checkpoint if available
238
343
  if checkpoint and os.path.exists(checkpoint):
239
344
  self._load_checkpoint(checkpoint)
240
345
 
241
- # Initialize context
242
- context: dict[str, Any] = {
243
- "raw_data": data,
244
- "flows": [],
245
- "payloads": [],
246
- "messages": [],
247
- "patterns": [],
248
- "clusters": [],
249
- "schemas": {},
250
- "protocol_candidates": [],
251
- "state_machine": None,
252
- "warnings": [],
253
- "statistics": {},
254
- }
255
-
256
- # Execute stages
257
- stage_results = []
258
- total_stages = len(self.stages)
259
-
260
- for i, stage_name in enumerate(self.stages):
261
- if stage_name in self._checkpoint_data:
262
- # Skip completed stages
263
- context.update(self._checkpoint_data[stage_name])
264
- continue
265
-
266
- self._report_progress(stage_name, (i / total_stages) * 100)
346
+ context = self._initialize_analysis_context(data)
267
347
 
268
- handler = self._stage_handlers.get(stage_name)
269
- if handler:
270
- try:
271
- stage_start = time.time()
272
- output = handler(context)
273
- stage_duration = time.time() - stage_start
274
-
275
- stage_results.append(
276
- StageResult(
277
- stage_name=stage_name,
278
- success=True,
279
- duration=stage_duration,
280
- output=output,
281
- )
282
- )
348
+ # Processing: execute pipeline stages
349
+ stage_results = self._execute_all_stages(context, checkpoint)
283
350
 
284
- # Update context with stage output
285
- if output:
286
- context.update(output)
287
-
288
- # Checkpoint after each stage
289
- if checkpoint:
290
- self._save_checkpoint(checkpoint, stage_name, context)
291
-
292
- except Exception as e:
293
- stage_results.append(
294
- StageResult(
295
- stage_name=stage_name,
296
- success=False,
297
- duration=0,
298
- output=None,
299
- error=str(e),
300
- )
301
- )
302
- warnings_list: list[str] = context.get("warnings", [])
303
- warnings_list.append(f"Stage {stage_name} failed: {e}")
304
- context["warnings"] = warnings_list
305
-
306
- self._report_progress("complete", 100)
307
-
308
- # Build result
351
+ # Result building: construct final result
309
352
  duration = time.time() - start_time
310
-
311
353
  flows_list: list[Any] = context.get("flows", [])
312
354
  messages_list: list[Any] = context.get("messages", [])
313
355
  protocol_candidates_list: list[ProtocolCandidate] = context.get("protocol_candidates", [])
@@ -416,115 +458,205 @@ class REPipeline:
416
458
  context: Pipeline context.
417
459
 
418
460
  Returns:
419
- Updated context with flows.
461
+ Updated context with flows and payloads.
420
462
  """
421
463
  data = context["raw_data"]
422
- flows = []
423
- payloads = []
424
464
 
425
465
  if isinstance(data, bytes):
426
- # Raw binary - treat as single payload
427
- payloads.append(data)
466
+ flows, payloads = self._extract_from_raw_bytes(data)
467
+ elif isinstance(data, list | tuple):
468
+ flows, payloads = self._extract_from_packet_list(data)
469
+ else:
470
+ flows, payloads = [], []
471
+
472
+ self._update_flow_statistics(context, flows, payloads)
473
+ return {"flows": flows, "payloads": payloads}
474
+
475
+ def _extract_from_raw_bytes(self, data: bytes) -> tuple[list[FlowInfo], list[bytes]]:
476
+ """Extract flow from raw binary data.
477
+
478
+ Args:
479
+ data: Raw binary data.
480
+
481
+ Returns:
482
+ Tuple of (flows, payloads).
483
+ """
484
+ flow = FlowInfo(
485
+ flow_id="flow_0",
486
+ src_ip="unknown",
487
+ dst_ip="unknown",
488
+ src_port=0,
489
+ dst_port=0,
490
+ protocol="unknown",
491
+ packet_count=1,
492
+ byte_count=len(data),
493
+ start_time=0,
494
+ end_time=0,
495
+ )
496
+ return [flow], [data]
497
+
498
+ def _extract_from_packet_list(
499
+ self, packets: Sequence[dict[str, Any] | bytes]
500
+ ) -> tuple[list[FlowInfo], list[bytes]]:
501
+ """Extract flows from list of packets.
502
+
503
+ Args:
504
+ packets: List of packet dicts or raw bytes.
505
+
506
+ Returns:
507
+ Tuple of (flows, payloads).
508
+ """
509
+ flow_map: dict[str, dict[str, Any]] = {}
510
+ payloads: list[bytes] = []
511
+ raw_bytes_payloads: list[bytes] = []
512
+
513
+ for pkt in packets:
514
+ if isinstance(pkt, dict):
515
+ self._process_packet_dict(pkt, flow_map, payloads)
516
+ else:
517
+ payload = bytes(pkt) if not isinstance(pkt, bytes) else pkt
518
+ payloads.append(payload)
519
+ raw_bytes_payloads.append(payload)
520
+
521
+ flows = self._build_flows_from_map(flow_map)
522
+
523
+ # Create default flow for raw bytes if needed
524
+ if raw_bytes_payloads and not flows:
525
+ flows.append(self._create_default_flow(raw_bytes_payloads))
526
+
527
+ return flows, payloads
528
+
529
+ def _process_packet_dict(
530
+ self,
531
+ pkt: dict[str, Any],
532
+ flow_map: dict[str, dict[str, Any]],
533
+ payloads: list[bytes],
534
+ ) -> None:
535
+ """Process a packet dictionary and update flow map.
536
+
537
+ Args:
538
+ pkt: Packet dictionary with metadata.
539
+ flow_map: Flow mapping to update.
540
+ payloads: Payloads list to append to.
541
+ """
542
+ # Extract payload
543
+ payload_raw = pkt.get("data", pkt.get("payload", b""))
544
+ if isinstance(payload_raw, list | tuple):
545
+ payload = bytes(payload_raw)
546
+ else:
547
+ payload = payload_raw if isinstance(payload_raw, bytes) else b""
548
+
549
+ # Create flow key
550
+ flow_key = self._create_flow_key(pkt)
551
+
552
+ # Initialize flow if new
553
+ if flow_key not in flow_map:
554
+ flow_map[flow_key] = self._create_flow_entry(pkt)
555
+
556
+ # Update flow data
557
+ flow_map[flow_key]["packets"].append(pkt)
558
+ flow_map[flow_key]["payloads"].append(payload)
559
+ if "timestamp" in pkt:
560
+ flow_map[flow_key]["timestamps"].append(pkt["timestamp"])
561
+
562
+ payloads.append(payload)
563
+
564
+ def _create_flow_key(self, pkt: dict[str, Any]) -> str:
565
+ """Create flow identifier key from packet.
566
+
567
+ Args:
568
+ pkt: Packet dictionary.
569
+
570
+ Returns:
571
+ Flow key string.
572
+ """
573
+ src_ip = pkt.get("src_ip", "0.0.0.0")
574
+ dst_ip = pkt.get("dst_ip", "0.0.0.0")
575
+ src_port = pkt.get("src_port", 0)
576
+ dst_port = pkt.get("dst_port", 0)
577
+ protocol = pkt.get("protocol", "unknown")
578
+ return f"{src_ip}:{src_port}-{dst_ip}:{dst_port}-{protocol}"
579
+
580
+ def _create_flow_entry(self, pkt: dict[str, Any]) -> dict[str, Any]:
581
+ """Create new flow entry from packet.
582
+
583
+ Args:
584
+ pkt: Packet dictionary.
585
+
586
+ Returns:
587
+ Flow entry dictionary.
588
+ """
589
+ return {
590
+ "src_ip": pkt.get("src_ip", "0.0.0.0"),
591
+ "dst_ip": pkt.get("dst_ip", "0.0.0.0"),
592
+ "src_port": pkt.get("src_port", 0),
593
+ "dst_port": pkt.get("dst_port", 0),
594
+ "protocol": pkt.get("protocol", "unknown"),
595
+ "packets": [],
596
+ "payloads": [],
597
+ "timestamps": [],
598
+ }
599
+
600
+ def _build_flows_from_map(self, flow_map: dict[str, dict[str, Any]]) -> list[FlowInfo]:
601
+ """Build FlowInfo objects from flow map.
602
+
603
+ Args:
604
+ flow_map: Mapping of flow keys to flow data.
605
+
606
+ Returns:
607
+ List of FlowInfo objects.
608
+ """
609
+ flows = []
610
+ for flow_id, flow_data in flow_map.items():
611
+ timestamps = flow_data.get("timestamps", [0])
428
612
  flows.append(
429
613
  FlowInfo(
430
- flow_id="flow_0",
431
- src_ip="unknown",
432
- dst_ip="unknown",
433
- src_port=0,
434
- dst_port=0,
435
- protocol="unknown",
436
- packet_count=1,
437
- byte_count=len(data),
438
- start_time=0,
439
- end_time=0,
614
+ flow_id=flow_id,
615
+ src_ip=flow_data["src_ip"],
616
+ dst_ip=flow_data["dst_ip"],
617
+ src_port=flow_data["src_port"],
618
+ dst_port=flow_data["dst_port"],
619
+ protocol=flow_data["protocol"],
620
+ packet_count=len(flow_data["packets"]),
621
+ byte_count=sum(len(p) for p in flow_data["payloads"]),
622
+ start_time=min(timestamps) if timestamps else 0,
623
+ end_time=max(timestamps) if timestamps else 0,
440
624
  )
441
625
  )
626
+ return flows
442
627
 
443
- elif isinstance(data, list | tuple):
444
- # List of packets
445
- flow_map: dict[str, dict[str, Any]] = {}
446
- raw_bytes_payloads: list[bytes] = []
447
-
448
- for _i, pkt in enumerate(data):
449
- if isinstance(pkt, dict):
450
- # Packet with metadata
451
- payload_raw = pkt.get("data", pkt.get("payload", b""))
452
- if isinstance(payload_raw, list | tuple):
453
- payload = bytes(payload_raw)
454
- else:
455
- payload = payload_raw if isinstance(payload_raw, bytes) else b""
456
-
457
- # Create flow key
458
- src_ip = pkt.get("src_ip", "0.0.0.0")
459
- dst_ip = pkt.get("dst_ip", "0.0.0.0")
460
- src_port = pkt.get("src_port", 0)
461
- dst_port = pkt.get("dst_port", 0)
462
- protocol = pkt.get("protocol", "unknown")
463
-
464
- flow_key = f"{src_ip}:{src_port}-{dst_ip}:{dst_port}-{protocol}"
465
-
466
- if flow_key not in flow_map:
467
- flow_map[flow_key] = {
468
- "src_ip": src_ip,
469
- "dst_ip": dst_ip,
470
- "src_port": src_port,
471
- "dst_port": dst_port,
472
- "protocol": protocol,
473
- "packets": [],
474
- "payloads": [],
475
- "timestamps": [],
476
- }
477
-
478
- flow_map[flow_key]["packets"].append(pkt)
479
- flow_map[flow_key]["payloads"].append(payload)
480
- if "timestamp" in pkt:
481
- flow_map[flow_key]["timestamps"].append(pkt["timestamp"])
482
-
483
- payloads.append(payload)
484
-
485
- else:
486
- # Raw bytes - collect for default flow
487
- raw_payload = bytes(pkt) if not isinstance(pkt, bytes) else pkt
488
- payloads.append(raw_payload)
489
- raw_bytes_payloads.append(raw_payload)
490
-
491
- # Build flow objects from flow_map
492
- for flow_id, flow_data in flow_map.items():
493
- timestamps = flow_data.get("timestamps", [0])
494
- flows.append(
495
- FlowInfo(
496
- flow_id=flow_id,
497
- src_ip=flow_data["src_ip"],
498
- dst_ip=flow_data["dst_ip"],
499
- src_port=flow_data["src_port"],
500
- dst_port=flow_data["dst_port"],
501
- protocol=flow_data["protocol"],
502
- packet_count=len(flow_data["packets"]),
503
- byte_count=sum(len(p) for p in flow_data["payloads"]),
504
- start_time=min(timestamps) if timestamps else 0,
505
- end_time=max(timestamps) if timestamps else 0,
506
- )
507
- )
628
+ def _create_default_flow(self, payloads: list[bytes]) -> FlowInfo:
629
+ """Create default flow for raw bytes.
508
630
 
509
- # Create a default flow for raw bytes if we have any
510
- # This ensures flow_count >= 1 when analyzing raw byte sequences
511
- if raw_bytes_payloads and not flows:
512
- flows.append(
513
- FlowInfo(
514
- flow_id="flow_default",
515
- src_ip="unknown",
516
- dst_ip="unknown",
517
- src_port=0,
518
- dst_port=0,
519
- protocol="unknown",
520
- packet_count=len(raw_bytes_payloads),
521
- byte_count=sum(len(p) for p in raw_bytes_payloads),
522
- start_time=0,
523
- end_time=0,
524
- )
525
- )
631
+ Args:
632
+ payloads: List of raw byte payloads.
526
633
 
527
- # Initialize statistics dict if it doesn't exist
634
+ Returns:
635
+ Default FlowInfo object.
636
+ """
637
+ return FlowInfo(
638
+ flow_id="flow_default",
639
+ src_ip="unknown",
640
+ dst_ip="unknown",
641
+ src_port=0,
642
+ dst_port=0,
643
+ protocol="unknown",
644
+ packet_count=len(payloads),
645
+ byte_count=sum(len(p) for p in payloads),
646
+ start_time=0,
647
+ end_time=0,
648
+ )
649
+
650
+ def _update_flow_statistics(
651
+ self, context: dict[str, Any], flows: list[FlowInfo], payloads: list[bytes]
652
+ ) -> None:
653
+ """Update context statistics with flow extraction results.
654
+
655
+ Args:
656
+ context: Pipeline context to update.
657
+ flows: Extracted flows.
658
+ payloads: Extracted payloads.
659
+ """
528
660
  if "statistics" not in context:
529
661
  context["statistics"] = {}
530
662
 
@@ -534,8 +666,6 @@ class REPipeline:
534
666
  "total_bytes": sum(len(p) for p in payloads),
535
667
  }
536
668
 
537
- return {"flows": flows, "payloads": payloads}
538
-
539
669
  def _stage_payload_analysis(self, context: dict[str, Any]) -> dict[str, Any]:
540
670
  """Analyze payloads for structure.
541
671
 
@@ -713,9 +843,27 @@ class REPipeline:
713
843
  """
714
844
  messages = context.get("messages", [])
715
845
  flows = context.get("flows", [])
716
- candidates = []
846
+ candidates: list[ProtocolCandidate] = []
847
+
848
+ # Detect protocols from multiple sources
849
+ candidates.extend(self._detect_by_port(flows))
850
+ candidates.extend(self._detect_by_magic_bytes(messages))
851
+ candidates.extend(self._detect_from_library(messages))
717
852
 
718
- # Check well-known port mappings
853
+ # Deduplicate candidates
854
+ unique_candidates = self._deduplicate_candidates(candidates)
855
+
856
+ return {"protocol_candidates": unique_candidates}
857
+
858
+ def _detect_by_port(self, flows: list[FlowInfo]) -> list[ProtocolCandidate]:
859
+ """Detect protocols based on well-known port numbers.
860
+
861
+ Args:
862
+ flows: List of network flows.
863
+
864
+ Returns:
865
+ List of protocol candidates.
866
+ """
719
867
  port_protocols = {
720
868
  53: "dns",
721
869
  80: "http",
@@ -726,6 +874,7 @@ class REPipeline:
726
874
  47808: "bacnet",
727
875
  }
728
876
 
877
+ candidates = []
729
878
  for flow in flows:
730
879
  port = flow.dst_port or flow.src_port
731
880
  if port in port_protocols:
@@ -737,69 +886,110 @@ class REPipeline:
737
886
  )
738
887
  )
739
888
 
740
- # Check magic byte signatures
741
- if messages:
742
- try:
743
- from oscura.inference.binary import MagicByteDetector
744
-
745
- detector = MagicByteDetector()
746
- sample = messages[0] if messages else b""
747
-
748
- if len(sample) >= 2:
749
- result = detector.detect(sample)
750
- if result and result.known_format:
751
- candidates.append(
752
- ProtocolCandidate(
753
- name=result.known_format,
754
- confidence=result.confidence,
755
- header_match=True,
756
- )
889
+ return candidates
890
+
891
+ def _detect_by_magic_bytes(self, messages: list[bytes]) -> list[ProtocolCandidate]:
892
+ """Detect protocols by magic byte signatures.
893
+
894
+ Args:
895
+ messages: List of message bytes.
896
+
897
+ Returns:
898
+ List of protocol candidates.
899
+ """
900
+ if not messages:
901
+ return []
902
+
903
+ try:
904
+ from oscura.inference.binary import MagicByteDetector
905
+
906
+ detector = MagicByteDetector()
907
+ sample = messages[0]
908
+
909
+ if len(sample) >= 2:
910
+ result = detector.detect(sample)
911
+ if result and result.known_format:
912
+ return [
913
+ ProtocolCandidate(
914
+ name=result.known_format,
915
+ confidence=result.confidence,
916
+ header_match=True,
757
917
  )
918
+ ]
758
919
 
759
- except Exception as e:
760
- logger.debug("Magic byte detection failed (non-critical): %s", e)
920
+ except Exception as e:
921
+ logger.debug("Magic byte detection failed (non-critical): %s", e)
922
+
923
+ return []
924
+
925
+ def _detect_from_library(self, messages: list[bytes]) -> list[ProtocolCandidate]:
926
+ """Detect protocols from protocol library.
761
927
 
762
- # Check protocol library
928
+ Args:
929
+ messages: List of message bytes.
930
+
931
+ Returns:
932
+ List of protocol candidates.
933
+ """
763
934
  try:
764
935
  from oscura.inference.protocol_library import get_library
765
936
 
766
937
  library = get_library()
938
+ candidates = []
767
939
 
768
940
  for protocol in library.list_protocols():
769
- if protocol.definition:
770
- # Check if first bytes match protocol header
771
- for msg in messages[:10]:
772
- if len(msg) >= 4:
773
- # Simple header matching
774
- first_field = (
775
- protocol.definition.fields[0]
776
- if protocol.definition.fields
777
- else None
778
- )
779
- if first_field and hasattr(first_field, "value"):
780
- # Has expected value
781
- candidates.append(
782
- ProtocolCandidate(
783
- name=protocol.name,
784
- confidence=0.4,
785
- matched_patterns=["header_value"],
786
- )
787
- )
788
- break
941
+ if self._matches_protocol_header(protocol, messages):
942
+ candidates.append(
943
+ ProtocolCandidate(
944
+ name=protocol.name,
945
+ confidence=0.4,
946
+ matched_patterns=["header_value"],
947
+ )
948
+ )
949
+
950
+ return candidates
789
951
 
790
952
  except Exception as e:
791
953
  logger.debug("Protocol library matching failed (non-critical): %s", e)
954
+ return []
955
+
956
+ def _matches_protocol_header(self, protocol: Any, messages: list[bytes]) -> bool:
957
+ """Check if messages match protocol header.
792
958
 
793
- # Deduplicate by name, keeping highest confidence
794
- unique_candidates: dict[str, ProtocolCandidate] = {}
959
+ Args:
960
+ protocol: Protocol definition.
961
+ messages: List of message bytes.
962
+
963
+ Returns:
964
+ True if matches.
965
+ """
966
+ if not protocol.definition or not protocol.definition.fields:
967
+ return False
968
+
969
+ first_field = protocol.definition.fields[0]
970
+ if not hasattr(first_field, "value"):
971
+ return False
972
+
973
+ # Check first 10 messages
974
+ return any(len(msg) >= 4 for msg in messages[:10])
975
+
976
+ def _deduplicate_candidates(
977
+ self, candidates: list[ProtocolCandidate]
978
+ ) -> list[ProtocolCandidate]:
979
+ """Deduplicate candidates, keeping highest confidence.
980
+
981
+ Args:
982
+ candidates: List of candidates.
983
+
984
+ Returns:
985
+ Deduplicated list.
986
+ """
987
+ unique: dict[str, ProtocolCandidate] = {}
795
988
  for c in candidates:
796
- if (
797
- c.name not in unique_candidates
798
- or c.confidence > unique_candidates[c.name].confidence
799
- ):
800
- unique_candidates[c.name] = c
989
+ if c.name not in unique or c.confidence > unique[c.name].confidence:
990
+ unique[c.name] = c
801
991
 
802
- return {"protocol_candidates": list(unique_candidates.values())}
992
+ return list(unique.values())
803
993
 
804
994
  def _stage_state_machine(self, context: dict[str, Any]) -> dict[str, Any]:
805
995
  """Infer protocol state machine.
@@ -825,7 +1015,7 @@ class REPipeline:
825
1015
  message_to_cluster[idx] = getattr(cluster, "cluster_id", 0)
826
1016
 
827
1017
  # Build observation sequence
828
- sequence = [
1018
+ sequence: list[str] = [
829
1019
  f"type_{message_to_cluster.get(i, 0)}"
830
1020
  for i in range(len(messages))
831
1021
  if i in message_to_cluster
@@ -835,7 +1025,8 @@ class REPipeline:
835
1025
  from oscura.inference.state_machine import StateMachineInferrer
836
1026
 
837
1027
  inferrer = StateMachineInferrer()
838
- automaton = inferrer.infer_rpni([sequence])
1028
+ # Cast list[str] to list[str | int] for API compatibility
1029
+ automaton = inferrer.infer_rpni([cast("list[str | int]", sequence)])
839
1030
 
840
1031
  return {
841
1032
  "state_machine": {