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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (497) hide show
  1. oscura/__init__.py +169 -167
  2. oscura/analyzers/__init__.py +3 -0
  3. oscura/analyzers/classification.py +659 -0
  4. oscura/analyzers/digital/edges.py +325 -65
  5. oscura/analyzers/digital/quality.py +293 -166
  6. oscura/analyzers/digital/timing.py +260 -115
  7. oscura/analyzers/digital/timing_numba.py +334 -0
  8. oscura/analyzers/entropy.py +605 -0
  9. oscura/analyzers/eye/diagram.py +176 -109
  10. oscura/analyzers/eye/metrics.py +5 -5
  11. oscura/analyzers/jitter/__init__.py +6 -4
  12. oscura/analyzers/jitter/ber.py +52 -52
  13. oscura/analyzers/jitter/classification.py +156 -0
  14. oscura/analyzers/jitter/decomposition.py +163 -113
  15. oscura/analyzers/jitter/spectrum.py +80 -64
  16. oscura/analyzers/ml/__init__.py +39 -0
  17. oscura/analyzers/ml/features.py +600 -0
  18. oscura/analyzers/ml/signal_classifier.py +604 -0
  19. oscura/analyzers/packet/daq.py +246 -158
  20. oscura/analyzers/packet/parser.py +12 -1
  21. oscura/analyzers/packet/payload.py +50 -2110
  22. oscura/analyzers/packet/payload_analysis.py +361 -181
  23. oscura/analyzers/packet/payload_patterns.py +133 -70
  24. oscura/analyzers/packet/stream.py +84 -23
  25. oscura/analyzers/patterns/__init__.py +26 -5
  26. oscura/analyzers/patterns/anomaly_detection.py +908 -0
  27. oscura/analyzers/patterns/clustering.py +169 -108
  28. oscura/analyzers/patterns/clustering_optimized.py +227 -0
  29. oscura/analyzers/patterns/discovery.py +1 -1
  30. oscura/analyzers/patterns/matching.py +581 -197
  31. oscura/analyzers/patterns/pattern_mining.py +778 -0
  32. oscura/analyzers/patterns/periodic.py +121 -38
  33. oscura/analyzers/patterns/sequences.py +175 -78
  34. oscura/analyzers/power/conduction.py +1 -1
  35. oscura/analyzers/power/soa.py +6 -6
  36. oscura/analyzers/power/switching.py +250 -110
  37. oscura/analyzers/protocol/__init__.py +17 -1
  38. oscura/analyzers/protocols/base.py +6 -6
  39. oscura/analyzers/protocols/ble/__init__.py +38 -0
  40. oscura/analyzers/protocols/ble/analyzer.py +809 -0
  41. oscura/analyzers/protocols/ble/uuids.py +288 -0
  42. oscura/analyzers/protocols/can.py +257 -127
  43. oscura/analyzers/protocols/can_fd.py +107 -80
  44. oscura/analyzers/protocols/flexray.py +139 -80
  45. oscura/analyzers/protocols/hdlc.py +93 -58
  46. oscura/analyzers/protocols/i2c.py +247 -106
  47. oscura/analyzers/protocols/i2s.py +138 -86
  48. oscura/analyzers/protocols/industrial/__init__.py +40 -0
  49. oscura/analyzers/protocols/industrial/bacnet/__init__.py +33 -0
  50. oscura/analyzers/protocols/industrial/bacnet/analyzer.py +708 -0
  51. oscura/analyzers/protocols/industrial/bacnet/encoding.py +412 -0
  52. oscura/analyzers/protocols/industrial/bacnet/services.py +622 -0
  53. oscura/analyzers/protocols/industrial/ethercat/__init__.py +30 -0
  54. oscura/analyzers/protocols/industrial/ethercat/analyzer.py +474 -0
  55. oscura/analyzers/protocols/industrial/ethercat/mailbox.py +339 -0
  56. oscura/analyzers/protocols/industrial/ethercat/topology.py +166 -0
  57. oscura/analyzers/protocols/industrial/modbus/__init__.py +31 -0
  58. oscura/analyzers/protocols/industrial/modbus/analyzer.py +525 -0
  59. oscura/analyzers/protocols/industrial/modbus/crc.py +79 -0
  60. oscura/analyzers/protocols/industrial/modbus/functions.py +436 -0
  61. oscura/analyzers/protocols/industrial/opcua/__init__.py +21 -0
  62. oscura/analyzers/protocols/industrial/opcua/analyzer.py +552 -0
  63. oscura/analyzers/protocols/industrial/opcua/datatypes.py +446 -0
  64. oscura/analyzers/protocols/industrial/opcua/services.py +264 -0
  65. oscura/analyzers/protocols/industrial/profinet/__init__.py +23 -0
  66. oscura/analyzers/protocols/industrial/profinet/analyzer.py +441 -0
  67. oscura/analyzers/protocols/industrial/profinet/dcp.py +263 -0
  68. oscura/analyzers/protocols/industrial/profinet/ptcp.py +200 -0
  69. oscura/analyzers/protocols/jtag.py +180 -98
  70. oscura/analyzers/protocols/lin.py +219 -114
  71. oscura/analyzers/protocols/manchester.py +4 -4
  72. oscura/analyzers/protocols/onewire.py +253 -149
  73. oscura/analyzers/protocols/parallel_bus/__init__.py +20 -0
  74. oscura/analyzers/protocols/parallel_bus/centronics.py +92 -0
  75. oscura/analyzers/protocols/parallel_bus/gpib.py +137 -0
  76. oscura/analyzers/protocols/spi.py +192 -95
  77. oscura/analyzers/protocols/swd.py +321 -167
  78. oscura/analyzers/protocols/uart.py +267 -125
  79. oscura/analyzers/protocols/usb.py +235 -131
  80. oscura/analyzers/side_channel/power.py +17 -12
  81. oscura/analyzers/signal/__init__.py +15 -0
  82. oscura/analyzers/signal/timing_analysis.py +1086 -0
  83. oscura/analyzers/signal_integrity/__init__.py +4 -1
  84. oscura/analyzers/signal_integrity/sparams.py +2 -19
  85. oscura/analyzers/spectral/chunked.py +129 -60
  86. oscura/analyzers/spectral/chunked_fft.py +300 -94
  87. oscura/analyzers/spectral/chunked_wavelet.py +100 -80
  88. oscura/analyzers/statistical/checksum.py +376 -217
  89. oscura/analyzers/statistical/classification.py +229 -107
  90. oscura/analyzers/statistical/entropy.py +78 -53
  91. oscura/analyzers/statistics/correlation.py +407 -211
  92. oscura/analyzers/statistics/outliers.py +2 -2
  93. oscura/analyzers/statistics/streaming.py +30 -5
  94. oscura/analyzers/validation.py +216 -101
  95. oscura/analyzers/waveform/measurements.py +9 -0
  96. oscura/analyzers/waveform/measurements_with_uncertainty.py +31 -15
  97. oscura/analyzers/waveform/spectral.py +500 -228
  98. oscura/api/__init__.py +31 -5
  99. oscura/api/dsl/__init__.py +582 -0
  100. oscura/{dsl → api/dsl}/commands.py +43 -76
  101. oscura/{dsl → api/dsl}/interpreter.py +26 -51
  102. oscura/{dsl → api/dsl}/parser.py +107 -77
  103. oscura/{dsl → api/dsl}/repl.py +2 -2
  104. oscura/api/dsl.py +1 -1
  105. oscura/{integrations → api/integrations}/__init__.py +1 -1
  106. oscura/{integrations → api/integrations}/llm.py +201 -102
  107. oscura/api/operators.py +3 -3
  108. oscura/api/optimization.py +144 -30
  109. oscura/api/rest_server.py +921 -0
  110. oscura/api/server/__init__.py +17 -0
  111. oscura/api/server/dashboard.py +850 -0
  112. oscura/api/server/static/README.md +34 -0
  113. oscura/api/server/templates/base.html +181 -0
  114. oscura/api/server/templates/export.html +120 -0
  115. oscura/api/server/templates/home.html +284 -0
  116. oscura/api/server/templates/protocols.html +58 -0
  117. oscura/api/server/templates/reports.html +43 -0
  118. oscura/api/server/templates/session_detail.html +89 -0
  119. oscura/api/server/templates/sessions.html +83 -0
  120. oscura/api/server/templates/waveforms.html +73 -0
  121. oscura/automotive/__init__.py +8 -1
  122. oscura/automotive/can/__init__.py +10 -0
  123. oscura/automotive/can/checksum.py +3 -1
  124. oscura/automotive/can/dbc_generator.py +590 -0
  125. oscura/automotive/can/message_wrapper.py +121 -74
  126. oscura/automotive/can/patterns.py +98 -21
  127. oscura/automotive/can/session.py +292 -56
  128. oscura/automotive/can/state_machine.py +6 -3
  129. oscura/automotive/can/stimulus_response.py +97 -75
  130. oscura/automotive/dbc/__init__.py +10 -2
  131. oscura/automotive/dbc/generator.py +84 -56
  132. oscura/automotive/dbc/parser.py +6 -6
  133. oscura/automotive/dtc/data.json +17 -102
  134. oscura/automotive/dtc/database.py +2 -2
  135. oscura/automotive/flexray/__init__.py +31 -0
  136. oscura/automotive/flexray/analyzer.py +504 -0
  137. oscura/automotive/flexray/crc.py +185 -0
  138. oscura/automotive/flexray/fibex.py +449 -0
  139. oscura/automotive/j1939/__init__.py +45 -8
  140. oscura/automotive/j1939/analyzer.py +605 -0
  141. oscura/automotive/j1939/spns.py +326 -0
  142. oscura/automotive/j1939/transport.py +306 -0
  143. oscura/automotive/lin/__init__.py +47 -0
  144. oscura/automotive/lin/analyzer.py +612 -0
  145. oscura/automotive/loaders/blf.py +13 -2
  146. oscura/automotive/loaders/csv_can.py +143 -72
  147. oscura/automotive/loaders/dispatcher.py +50 -2
  148. oscura/automotive/loaders/mdf.py +86 -45
  149. oscura/automotive/loaders/pcap.py +111 -61
  150. oscura/automotive/uds/__init__.py +4 -0
  151. oscura/automotive/uds/analyzer.py +725 -0
  152. oscura/automotive/uds/decoder.py +140 -58
  153. oscura/automotive/uds/models.py +7 -1
  154. oscura/automotive/visualization.py +1 -1
  155. oscura/cli/analyze.py +348 -0
  156. oscura/cli/batch.py +142 -122
  157. oscura/cli/benchmark.py +275 -0
  158. oscura/cli/characterize.py +137 -82
  159. oscura/cli/compare.py +224 -131
  160. oscura/cli/completion.py +250 -0
  161. oscura/cli/config_cmd.py +361 -0
  162. oscura/cli/decode.py +164 -87
  163. oscura/cli/export.py +286 -0
  164. oscura/cli/main.py +115 -31
  165. oscura/{onboarding → cli/onboarding}/__init__.py +3 -3
  166. oscura/{onboarding → cli/onboarding}/help.py +80 -58
  167. oscura/{onboarding → cli/onboarding}/tutorials.py +97 -72
  168. oscura/{onboarding → cli/onboarding}/wizard.py +55 -36
  169. oscura/cli/progress.py +147 -0
  170. oscura/cli/shell.py +157 -135
  171. oscura/cli/validate_cmd.py +204 -0
  172. oscura/cli/visualize.py +158 -0
  173. oscura/convenience.py +125 -79
  174. oscura/core/__init__.py +4 -2
  175. oscura/core/backend_selector.py +3 -3
  176. oscura/core/cache.py +126 -15
  177. oscura/core/cancellation.py +1 -1
  178. oscura/{config → core/config}/__init__.py +20 -11
  179. oscura/{config → core/config}/defaults.py +1 -1
  180. oscura/{config → core/config}/loader.py +7 -5
  181. oscura/{config → core/config}/memory.py +5 -5
  182. oscura/{config → core/config}/migration.py +1 -1
  183. oscura/{config → core/config}/pipeline.py +99 -23
  184. oscura/{config → core/config}/preferences.py +1 -1
  185. oscura/{config → core/config}/protocol.py +3 -3
  186. oscura/{config → core/config}/schema.py +426 -272
  187. oscura/{config → core/config}/settings.py +1 -1
  188. oscura/{config → core/config}/thresholds.py +195 -153
  189. oscura/core/correlation.py +5 -6
  190. oscura/core/cross_domain.py +0 -2
  191. oscura/core/debug.py +9 -5
  192. oscura/{extensibility → core/extensibility}/docs.py +158 -70
  193. oscura/{extensibility → core/extensibility}/extensions.py +160 -76
  194. oscura/{extensibility → core/extensibility}/logging.py +1 -1
  195. oscura/{extensibility → core/extensibility}/measurements.py +1 -1
  196. oscura/{extensibility → core/extensibility}/plugins.py +1 -1
  197. oscura/{extensibility → core/extensibility}/templates.py +73 -3
  198. oscura/{extensibility → core/extensibility}/validation.py +1 -1
  199. oscura/core/gpu_backend.py +11 -7
  200. oscura/core/log_query.py +101 -11
  201. oscura/core/logging.py +126 -54
  202. oscura/core/logging_advanced.py +5 -5
  203. oscura/core/memory_limits.py +108 -70
  204. oscura/core/memory_monitor.py +2 -2
  205. oscura/core/memory_progress.py +7 -7
  206. oscura/core/memory_warnings.py +1 -1
  207. oscura/core/numba_backend.py +13 -13
  208. oscura/{plugins → core/plugins}/__init__.py +9 -9
  209. oscura/{plugins → core/plugins}/base.py +7 -7
  210. oscura/{plugins → core/plugins}/cli.py +3 -3
  211. oscura/{plugins → core/plugins}/discovery.py +186 -106
  212. oscura/{plugins → core/plugins}/lifecycle.py +1 -1
  213. oscura/{plugins → core/plugins}/manager.py +7 -7
  214. oscura/{plugins → core/plugins}/registry.py +3 -3
  215. oscura/{plugins → core/plugins}/versioning.py +1 -1
  216. oscura/core/progress.py +16 -1
  217. oscura/core/provenance.py +8 -2
  218. oscura/{schemas → core/schemas}/__init__.py +2 -2
  219. oscura/{schemas → core/schemas}/device_mapping.json +2 -8
  220. oscura/{schemas → core/schemas}/packet_format.json +4 -24
  221. oscura/{schemas → core/schemas}/protocol_definition.json +2 -12
  222. oscura/core/types.py +4 -0
  223. oscura/core/uncertainty.py +3 -3
  224. oscura/correlation/__init__.py +52 -0
  225. oscura/correlation/multi_protocol.py +811 -0
  226. oscura/discovery/auto_decoder.py +117 -35
  227. oscura/discovery/comparison.py +191 -86
  228. oscura/discovery/quality_validator.py +155 -68
  229. oscura/discovery/signal_detector.py +196 -79
  230. oscura/export/__init__.py +18 -8
  231. oscura/export/kaitai_struct.py +513 -0
  232. oscura/export/scapy_layer.py +801 -0
  233. oscura/export/wireshark/generator.py +1 -1
  234. oscura/export/wireshark/templates/dissector.lua.j2 +2 -2
  235. oscura/export/wireshark_dissector.py +746 -0
  236. oscura/guidance/wizard.py +207 -111
  237. oscura/hardware/__init__.py +19 -0
  238. oscura/{acquisition → hardware/acquisition}/__init__.py +4 -4
  239. oscura/{acquisition → hardware/acquisition}/file.py +2 -2
  240. oscura/{acquisition → hardware/acquisition}/hardware.py +7 -7
  241. oscura/{acquisition → hardware/acquisition}/saleae.py +15 -12
  242. oscura/{acquisition → hardware/acquisition}/socketcan.py +1 -1
  243. oscura/{acquisition → hardware/acquisition}/streaming.py +2 -2
  244. oscura/{acquisition → hardware/acquisition}/synthetic.py +3 -3
  245. oscura/{acquisition → hardware/acquisition}/visa.py +33 -11
  246. oscura/hardware/firmware/__init__.py +29 -0
  247. oscura/hardware/firmware/pattern_recognition.py +874 -0
  248. oscura/hardware/hal_detector.py +736 -0
  249. oscura/hardware/security/__init__.py +37 -0
  250. oscura/hardware/security/side_channel_detector.py +1126 -0
  251. oscura/inference/__init__.py +4 -0
  252. oscura/inference/active_learning/observation_table.py +4 -1
  253. oscura/inference/alignment.py +216 -123
  254. oscura/inference/bayesian.py +113 -33
  255. oscura/inference/crc_reverse.py +101 -55
  256. oscura/inference/logic.py +6 -2
  257. oscura/inference/message_format.py +342 -183
  258. oscura/inference/protocol.py +95 -44
  259. oscura/inference/protocol_dsl.py +180 -82
  260. oscura/inference/signal_intelligence.py +1439 -706
  261. oscura/inference/spectral.py +99 -57
  262. oscura/inference/state_machine.py +810 -158
  263. oscura/inference/stream.py +270 -110
  264. oscura/iot/__init__.py +34 -0
  265. oscura/iot/coap/__init__.py +32 -0
  266. oscura/iot/coap/analyzer.py +668 -0
  267. oscura/iot/coap/options.py +212 -0
  268. oscura/iot/lorawan/__init__.py +21 -0
  269. oscura/iot/lorawan/crypto.py +206 -0
  270. oscura/iot/lorawan/decoder.py +801 -0
  271. oscura/iot/lorawan/mac_commands.py +341 -0
  272. oscura/iot/mqtt/__init__.py +27 -0
  273. oscura/iot/mqtt/analyzer.py +999 -0
  274. oscura/iot/mqtt/properties.py +315 -0
  275. oscura/iot/zigbee/__init__.py +31 -0
  276. oscura/iot/zigbee/analyzer.py +615 -0
  277. oscura/iot/zigbee/security.py +153 -0
  278. oscura/iot/zigbee/zcl.py +349 -0
  279. oscura/jupyter/display.py +125 -45
  280. oscura/{exploratory → jupyter/exploratory}/__init__.py +8 -8
  281. oscura/{exploratory → jupyter/exploratory}/error_recovery.py +298 -141
  282. oscura/jupyter/exploratory/fuzzy.py +746 -0
  283. oscura/{exploratory → jupyter/exploratory}/fuzzy_advanced.py +258 -100
  284. oscura/{exploratory → jupyter/exploratory}/legacy.py +464 -242
  285. oscura/{exploratory → jupyter/exploratory}/parse.py +167 -145
  286. oscura/{exploratory → jupyter/exploratory}/recovery.py +119 -87
  287. oscura/jupyter/exploratory/sync.py +612 -0
  288. oscura/{exploratory → jupyter/exploratory}/unknown.py +299 -176
  289. oscura/jupyter/magic.py +4 -4
  290. oscura/{ui → jupyter/ui}/__init__.py +2 -2
  291. oscura/{ui → jupyter/ui}/formatters.py +3 -3
  292. oscura/{ui → jupyter/ui}/progressive_display.py +153 -82
  293. oscura/loaders/__init__.py +183 -67
  294. oscura/loaders/binary.py +88 -1
  295. oscura/loaders/chipwhisperer.py +153 -137
  296. oscura/loaders/configurable.py +208 -86
  297. oscura/loaders/csv_loader.py +458 -215
  298. oscura/loaders/hdf5_loader.py +278 -119
  299. oscura/loaders/lazy.py +87 -54
  300. oscura/loaders/mmap_loader.py +1 -1
  301. oscura/loaders/numpy_loader.py +253 -116
  302. oscura/loaders/pcap.py +226 -151
  303. oscura/loaders/rigol.py +110 -49
  304. oscura/loaders/sigrok.py +201 -78
  305. oscura/loaders/tdms.py +81 -58
  306. oscura/loaders/tektronix.py +291 -174
  307. oscura/loaders/touchstone.py +182 -87
  308. oscura/loaders/tss.py +456 -0
  309. oscura/loaders/vcd.py +215 -117
  310. oscura/loaders/wav.py +155 -68
  311. oscura/reporting/__init__.py +9 -0
  312. oscura/reporting/analyze.py +352 -146
  313. oscura/reporting/argument_preparer.py +69 -14
  314. oscura/reporting/auto_report.py +97 -61
  315. oscura/reporting/batch.py +131 -58
  316. oscura/reporting/chart_selection.py +57 -45
  317. oscura/reporting/comparison.py +63 -17
  318. oscura/reporting/content/executive.py +76 -24
  319. oscura/reporting/core_formats/multi_format.py +11 -8
  320. oscura/reporting/engine.py +312 -158
  321. oscura/reporting/enhanced_reports.py +949 -0
  322. oscura/reporting/export.py +86 -43
  323. oscura/reporting/formatting/numbers.py +69 -42
  324. oscura/reporting/html.py +139 -58
  325. oscura/reporting/index.py +137 -65
  326. oscura/reporting/output.py +158 -67
  327. oscura/reporting/pdf.py +67 -102
  328. oscura/reporting/plots.py +191 -112
  329. oscura/reporting/sections.py +88 -47
  330. oscura/reporting/standards.py +104 -61
  331. oscura/reporting/summary_generator.py +75 -55
  332. oscura/reporting/tables.py +138 -54
  333. oscura/reporting/templates/enhanced/protocol_re.html +525 -0
  334. oscura/sessions/__init__.py +14 -23
  335. oscura/sessions/base.py +3 -3
  336. oscura/sessions/blackbox.py +106 -10
  337. oscura/sessions/generic.py +2 -2
  338. oscura/sessions/legacy.py +783 -0
  339. oscura/side_channel/__init__.py +63 -0
  340. oscura/side_channel/dpa.py +1025 -0
  341. oscura/utils/__init__.py +15 -1
  342. oscura/utils/bitwise.py +118 -0
  343. oscura/{builders → utils/builders}/__init__.py +1 -1
  344. oscura/{comparison → utils/comparison}/__init__.py +6 -6
  345. oscura/{comparison → utils/comparison}/compare.py +202 -101
  346. oscura/{comparison → utils/comparison}/golden.py +83 -63
  347. oscura/{comparison → utils/comparison}/limits.py +313 -89
  348. oscura/{comparison → utils/comparison}/mask.py +151 -45
  349. oscura/{comparison → utils/comparison}/trace_diff.py +1 -1
  350. oscura/{comparison → utils/comparison}/visualization.py +147 -89
  351. oscura/{component → utils/component}/__init__.py +3 -3
  352. oscura/{component → utils/component}/impedance.py +122 -58
  353. oscura/{component → utils/component}/reactive.py +165 -168
  354. oscura/{component → utils/component}/transmission_line.py +3 -3
  355. oscura/{filtering → utils/filtering}/__init__.py +6 -6
  356. oscura/{filtering → utils/filtering}/base.py +1 -1
  357. oscura/{filtering → utils/filtering}/convenience.py +2 -2
  358. oscura/{filtering → utils/filtering}/design.py +169 -93
  359. oscura/{filtering → utils/filtering}/filters.py +2 -2
  360. oscura/{filtering → utils/filtering}/introspection.py +2 -2
  361. oscura/utils/geometry.py +31 -0
  362. oscura/utils/imports.py +184 -0
  363. oscura/utils/lazy.py +1 -1
  364. oscura/{math → utils/math}/__init__.py +2 -2
  365. oscura/{math → utils/math}/arithmetic.py +114 -48
  366. oscura/{math → utils/math}/interpolation.py +139 -106
  367. oscura/utils/memory.py +129 -66
  368. oscura/utils/memory_advanced.py +92 -9
  369. oscura/utils/memory_extensions.py +10 -8
  370. oscura/{optimization → utils/optimization}/__init__.py +1 -1
  371. oscura/{optimization → utils/optimization}/search.py +2 -2
  372. oscura/utils/performance/__init__.py +58 -0
  373. oscura/utils/performance/caching.py +889 -0
  374. oscura/utils/performance/lsh_clustering.py +333 -0
  375. oscura/utils/performance/memory_optimizer.py +699 -0
  376. oscura/utils/performance/optimizations.py +675 -0
  377. oscura/utils/performance/parallel.py +654 -0
  378. oscura/utils/performance/profiling.py +661 -0
  379. oscura/{pipeline → utils/pipeline}/base.py +1 -1
  380. oscura/{pipeline → utils/pipeline}/composition.py +1 -1
  381. oscura/{pipeline → utils/pipeline}/parallel.py +3 -2
  382. oscura/{pipeline → utils/pipeline}/pipeline.py +1 -1
  383. oscura/{pipeline → utils/pipeline}/reverse_engineering.py +412 -221
  384. oscura/{search → utils/search}/__init__.py +3 -3
  385. oscura/{search → utils/search}/anomaly.py +188 -58
  386. oscura/utils/search/context.py +294 -0
  387. oscura/{search → utils/search}/pattern.py +138 -10
  388. oscura/utils/serial.py +51 -0
  389. oscura/utils/storage/__init__.py +61 -0
  390. oscura/utils/storage/database.py +1166 -0
  391. oscura/{streaming → utils/streaming}/chunked.py +302 -143
  392. oscura/{streaming → utils/streaming}/progressive.py +1 -1
  393. oscura/{streaming → utils/streaming}/realtime.py +3 -2
  394. oscura/{triggering → utils/triggering}/__init__.py +6 -6
  395. oscura/{triggering → utils/triggering}/base.py +6 -6
  396. oscura/{triggering → utils/triggering}/edge.py +2 -2
  397. oscura/{triggering → utils/triggering}/pattern.py +2 -2
  398. oscura/{triggering → utils/triggering}/pulse.py +115 -74
  399. oscura/{triggering → utils/triggering}/window.py +2 -2
  400. oscura/utils/validation.py +32 -0
  401. oscura/validation/__init__.py +121 -0
  402. oscura/{compliance → validation/compliance}/__init__.py +5 -5
  403. oscura/{compliance → validation/compliance}/advanced.py +5 -5
  404. oscura/{compliance → validation/compliance}/masks.py +1 -1
  405. oscura/{compliance → validation/compliance}/reporting.py +127 -53
  406. oscura/{compliance → validation/compliance}/testing.py +114 -52
  407. oscura/validation/compliance_tests.py +915 -0
  408. oscura/validation/fuzzer.py +990 -0
  409. oscura/validation/grammar_tests.py +596 -0
  410. oscura/validation/grammar_validator.py +904 -0
  411. oscura/validation/hil_testing.py +977 -0
  412. oscura/{quality → validation/quality}/__init__.py +4 -4
  413. oscura/{quality → validation/quality}/ensemble.py +251 -171
  414. oscura/{quality → validation/quality}/explainer.py +3 -3
  415. oscura/{quality → validation/quality}/scoring.py +1 -1
  416. oscura/{quality → validation/quality}/warnings.py +4 -4
  417. oscura/validation/regression_suite.py +808 -0
  418. oscura/validation/replay.py +788 -0
  419. oscura/{testing → validation/testing}/__init__.py +2 -2
  420. oscura/{testing → validation/testing}/synthetic.py +5 -5
  421. oscura/visualization/__init__.py +9 -0
  422. oscura/visualization/accessibility.py +1 -1
  423. oscura/visualization/annotations.py +64 -67
  424. oscura/visualization/colors.py +7 -7
  425. oscura/visualization/digital.py +180 -81
  426. oscura/visualization/eye.py +236 -85
  427. oscura/visualization/interactive.py +320 -143
  428. oscura/visualization/jitter.py +587 -247
  429. oscura/visualization/layout.py +169 -134
  430. oscura/visualization/optimization.py +103 -52
  431. oscura/visualization/palettes.py +1 -1
  432. oscura/visualization/power.py +427 -211
  433. oscura/visualization/power_extended.py +626 -297
  434. oscura/visualization/presets.py +2 -0
  435. oscura/visualization/protocols.py +495 -181
  436. oscura/visualization/render.py +79 -63
  437. oscura/visualization/reverse_engineering.py +171 -124
  438. oscura/visualization/signal_integrity.py +460 -279
  439. oscura/visualization/specialized.py +190 -100
  440. oscura/visualization/spectral.py +670 -255
  441. oscura/visualization/thumbnails.py +166 -137
  442. oscura/visualization/waveform.py +150 -63
  443. oscura/workflows/__init__.py +3 -0
  444. oscura/{batch → workflows/batch}/__init__.py +5 -5
  445. oscura/{batch → workflows/batch}/advanced.py +150 -75
  446. oscura/workflows/batch/aggregate.py +531 -0
  447. oscura/workflows/batch/analyze.py +236 -0
  448. oscura/{batch → workflows/batch}/logging.py +2 -2
  449. oscura/{batch → workflows/batch}/metrics.py +1 -1
  450. oscura/workflows/complete_re.py +1144 -0
  451. oscura/workflows/compliance.py +44 -54
  452. oscura/workflows/digital.py +197 -51
  453. oscura/workflows/legacy/__init__.py +12 -0
  454. oscura/{workflow → workflows/legacy}/dag.py +4 -1
  455. oscura/workflows/multi_trace.py +9 -9
  456. oscura/workflows/power.py +42 -62
  457. oscura/workflows/protocol.py +82 -49
  458. oscura/workflows/reverse_engineering.py +351 -150
  459. oscura/workflows/signal_integrity.py +157 -82
  460. oscura-0.7.0.dist-info/METADATA +661 -0
  461. oscura-0.7.0.dist-info/RECORD +591 -0
  462. oscura/batch/aggregate.py +0 -300
  463. oscura/batch/analyze.py +0 -139
  464. oscura/dsl/__init__.py +0 -73
  465. oscura/exceptions.py +0 -59
  466. oscura/exploratory/fuzzy.py +0 -513
  467. oscura/exploratory/sync.py +0 -384
  468. oscura/exporters/__init__.py +0 -94
  469. oscura/exporters/csv.py +0 -303
  470. oscura/exporters/exporters.py +0 -44
  471. oscura/exporters/hdf5.py +0 -217
  472. oscura/exporters/html_export.py +0 -701
  473. oscura/exporters/json_export.py +0 -291
  474. oscura/exporters/markdown_export.py +0 -367
  475. oscura/exporters/matlab_export.py +0 -354
  476. oscura/exporters/npz_export.py +0 -219
  477. oscura/exporters/spice_export.py +0 -210
  478. oscura/search/context.py +0 -149
  479. oscura/session/__init__.py +0 -34
  480. oscura/session/annotations.py +0 -289
  481. oscura/session/history.py +0 -313
  482. oscura/session/session.py +0 -520
  483. oscura/workflow/__init__.py +0 -13
  484. oscura-0.5.1.dist-info/METADATA +0 -583
  485. oscura-0.5.1.dist-info/RECORD +0 -481
  486. /oscura/core/{config.py → config/legacy.py} +0 -0
  487. /oscura/{extensibility → core/extensibility}/__init__.py +0 -0
  488. /oscura/{extensibility → core/extensibility}/registry.py +0 -0
  489. /oscura/{plugins → core/plugins}/isolation.py +0 -0
  490. /oscura/{schemas → core/schemas}/bus_configuration.json +0 -0
  491. /oscura/{builders → utils/builders}/signal_builder.py +0 -0
  492. /oscura/{optimization → utils/optimization}/parallel.py +0 -0
  493. /oscura/{pipeline → utils/pipeline}/__init__.py +0 -0
  494. /oscura/{streaming → utils/streaming}/__init__.py +0 -0
  495. {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/WHEEL +0 -0
  496. {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/entry_points.txt +0 -0
  497. {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,850 @@
1
+ """Interactive web dashboard for protocol analysis.
2
+
3
+ This module provides a comprehensive web-based UI for Oscura, enabling:
4
+ - File upload and analysis management
5
+ - Real-time progress tracking
6
+ - Interactive waveform visualization
7
+ - Protocol exploration and browsing
8
+ - Export to multiple formats (Wireshark, Scapy, Kaitai)
9
+ - Session management interface
10
+
11
+ The dashboard uses FastAPI for the backend with Jinja2 templates for
12
+ HTML rendering. Real-time updates are provided via WebSocket connections.
13
+
14
+ Example:
15
+ >>> from oscura.api.server import WebDashboard
16
+ >>> dashboard = WebDashboard(host="0.0.0.0", port=5000)
17
+ >>> dashboard.run() # Starts server on http://0.0.0.0:5000
18
+
19
+ Architecture:
20
+ - FastAPI backend (REST API + WebSocket)
21
+ - Jinja2 templates for HTML pages
22
+ - Bootstrap CSS framework for responsive UI
23
+ - Plotly.js for interactive waveform visualization
24
+ - Vanilla JavaScript for client-side interactivity
25
+ - Optional dark/light theme support
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import json
31
+ import logging
32
+ from dataclasses import dataclass, field
33
+ from datetime import datetime
34
+ from pathlib import Path
35
+ from typing import Any
36
+
37
+ logger = logging.getLogger(__name__)
38
+
39
+ # Try to import FastAPI and dependencies
40
+ try:
41
+ from fastapi import (
42
+ BackgroundTasks,
43
+ FastAPI,
44
+ HTTPException,
45
+ Request,
46
+ UploadFile,
47
+ WebSocket,
48
+ WebSocketDisconnect,
49
+ status,
50
+ )
51
+ from fastapi.middleware.cors import CORSMiddleware
52
+ from fastapi.responses import (
53
+ FileResponse,
54
+ HTMLResponse,
55
+ JSONResponse,
56
+ )
57
+ from fastapi.staticfiles import StaticFiles
58
+ from fastapi.templating import Jinja2Templates
59
+
60
+ HAS_FASTAPI = True
61
+ except ImportError:
62
+ HAS_FASTAPI = False
63
+ logger.warning("FastAPI not available. Install with: pip install 'fastapi[all]' uvicorn")
64
+
65
+
66
+ # ============================================================================
67
+ # Dashboard Configuration
68
+ # ============================================================================
69
+
70
+
71
+ @dataclass
72
+ class DashboardConfig:
73
+ """Configuration for web dashboard.
74
+
75
+ Attributes:
76
+ title: Dashboard title displayed in UI.
77
+ theme: Default theme (light or dark).
78
+ max_file_size: Maximum upload file size in bytes.
79
+ enable_websocket: Enable WebSocket for real-time updates.
80
+ session_timeout: Session timeout in seconds.
81
+ cache_waveforms: Cache waveform data for faster display.
82
+ plotly_config: Configuration for Plotly.js charts.
83
+ """
84
+
85
+ title: str = "Oscura Protocol Analysis Dashboard"
86
+ theme: str = "dark"
87
+ max_file_size: int = 100 * 1024 * 1024 # 100 MB
88
+ enable_websocket: bool = True
89
+ session_timeout: float = 3600.0 # 1 hour
90
+ cache_waveforms: bool = True
91
+ plotly_config: dict[str, Any] = field(
92
+ default_factory=lambda: {
93
+ "responsive": True,
94
+ "displayModeBar": True,
95
+ "displaylogo": False,
96
+ "toImageButtonOptions": {
97
+ "format": "png",
98
+ "filename": "oscura_waveform",
99
+ "height": 800,
100
+ "width": 1200,
101
+ },
102
+ }
103
+ )
104
+
105
+
106
+ # ============================================================================
107
+ # WebSocket Connection Manager
108
+ # ============================================================================
109
+
110
+
111
+ class ConnectionManager:
112
+ """Manages WebSocket connections for real-time updates.
113
+
114
+ Attributes:
115
+ active_connections: Dict mapping session_id to WebSocket connections.
116
+ """
117
+
118
+ def __init__(self) -> None:
119
+ """Initialize connection manager."""
120
+ self.active_connections: dict[str, list[WebSocket]] = {}
121
+
122
+ async def connect(self, websocket: WebSocket, session_id: str) -> None:
123
+ """Accept WebSocket connection.
124
+
125
+ Args:
126
+ websocket: WebSocket connection.
127
+ session_id: Session identifier.
128
+ """
129
+ await websocket.accept()
130
+ if session_id not in self.active_connections:
131
+ self.active_connections[session_id] = []
132
+ self.active_connections[session_id].append(websocket)
133
+ logger.info(f"WebSocket connected for session {session_id}")
134
+
135
+ def disconnect(self, websocket: WebSocket, session_id: str) -> None:
136
+ """Remove WebSocket connection.
137
+
138
+ Args:
139
+ websocket: WebSocket connection.
140
+ session_id: Session identifier.
141
+ """
142
+ if session_id in self.active_connections:
143
+ self.active_connections[session_id].remove(websocket)
144
+ if not self.active_connections[session_id]:
145
+ del self.active_connections[session_id]
146
+ logger.info(f"WebSocket disconnected for session {session_id}")
147
+
148
+ async def send_message(self, session_id: str, message: dict[str, Any]) -> None:
149
+ """Send message to all connections for a session.
150
+
151
+ Args:
152
+ session_id: Session identifier.
153
+ message: Message to send (will be JSON-encoded).
154
+ """
155
+ if session_id in self.active_connections:
156
+ for connection in self.active_connections[session_id]:
157
+ try:
158
+ await connection.send_json(message)
159
+ except Exception as e:
160
+ logger.error(f"Failed to send WebSocket message: {e}")
161
+
162
+ async def broadcast(self, message: dict[str, Any]) -> None:
163
+ """Broadcast message to all active connections.
164
+
165
+ Args:
166
+ message: Message to send (will be JSON-encoded).
167
+ """
168
+ for connections in self.active_connections.values():
169
+ for connection in connections:
170
+ try:
171
+ await connection.send_json(message)
172
+ except Exception as e:
173
+ logger.error(f"Failed to broadcast WebSocket message: {e}")
174
+
175
+
176
+ # ============================================================================
177
+ # Web Dashboard
178
+ # ============================================================================
179
+
180
+
181
+ class WebDashboard:
182
+ """Interactive web dashboard for Oscura protocol analysis.
183
+
184
+ Provides comprehensive web-based UI for:
185
+ - File upload and analysis management
186
+ - Real-time progress tracking via WebSocket
187
+ - Interactive waveform visualization with Plotly.js
188
+ - Protocol exploration and browsing
189
+ - Export to multiple formats
190
+ - Session management interface
191
+ - Dark/light theme toggle
192
+ - Responsive mobile-friendly design
193
+
194
+ Example:
195
+ >>> dashboard = WebDashboard(host="0.0.0.0", port=5000)
196
+ >>> dashboard.run()
197
+ >>> # Visit http://0.0.0.0:5000 for web UI
198
+
199
+ Architecture:
200
+ - FastAPI backend for REST API endpoints
201
+ - Jinja2 templates for server-side rendering
202
+ - WebSocket for real-time progress updates
203
+ - Bootstrap for responsive CSS framework
204
+ - Plotly.js for interactive charts
205
+ - Vanilla JavaScript for client-side logic
206
+ """
207
+
208
+ def __init__(
209
+ self,
210
+ host: str = "127.0.0.1",
211
+ port: int = 5000,
212
+ config: DashboardConfig | None = None,
213
+ api_server: Any | None = None,
214
+ ):
215
+ """Initialize web dashboard.
216
+
217
+ Args:
218
+ host: Server host address.
219
+ port: Server port number.
220
+ config: Dashboard configuration.
221
+ api_server: Optional RESTAPIServer instance (for session management).
222
+
223
+ Raises:
224
+ ImportError: If FastAPI is not available.
225
+ """
226
+ if not HAS_FASTAPI:
227
+ raise ImportError("FastAPI required. Install with: pip install 'fastapi[all]' uvicorn")
228
+
229
+ self.host = host
230
+ self.port = port
231
+ self.config = config or DashboardConfig()
232
+
233
+ # Import REST API server for session management
234
+ if api_server is None:
235
+ from oscura.api.rest_server import RESTAPIServer
236
+
237
+ self.api_server = RESTAPIServer(host=host, port=8000)
238
+ else:
239
+ self.api_server = api_server
240
+
241
+ # WebSocket connection manager
242
+ self.ws_manager = ConnectionManager()
243
+
244
+ # Create FastAPI app with dynamic version from package metadata (SSOT: pyproject.toml)
245
+ try:
246
+ from importlib.metadata import version
247
+
248
+ app_version = version("oscura")
249
+ except Exception:
250
+ app_version = "0.0.0+dev"
251
+
252
+ self.app = FastAPI(
253
+ title=self.config.title,
254
+ description="Interactive web dashboard for hardware reverse engineering",
255
+ version=app_version,
256
+ )
257
+
258
+ # Add CORS middleware
259
+ self.app.add_middleware(
260
+ CORSMiddleware,
261
+ allow_origins=["*"],
262
+ allow_credentials=True,
263
+ allow_methods=["*"],
264
+ allow_headers=["*"],
265
+ )
266
+
267
+ # Setup templates and static files
268
+ self._setup_templates()
269
+
270
+ # Register routes
271
+ self._register_routes()
272
+
273
+ def _setup_templates(self) -> None:
274
+ """Setup Jinja2 templates and static files."""
275
+ # Get path to web module
276
+ web_dir = Path(__file__).parent
277
+
278
+ # Templates directory
279
+ templates_dir = web_dir / "templates"
280
+ if not templates_dir.exists():
281
+ templates_dir.mkdir(parents=True)
282
+ logger.warning(f"Templates directory created: {templates_dir}")
283
+
284
+ self.templates = Jinja2Templates(directory=str(templates_dir))
285
+
286
+ # Static files directory
287
+ static_dir = web_dir / "static"
288
+ if not static_dir.exists():
289
+ static_dir.mkdir(parents=True)
290
+ logger.warning(f"Static directory created: {static_dir}")
291
+
292
+ # Mount static files
293
+ try:
294
+ self.app.mount("/static", StaticFiles(directory=str(static_dir)), name="static")
295
+ except Exception as e:
296
+ logger.warning(f"Could not mount static files: {e}")
297
+
298
+ def _register_routes(self) -> None:
299
+ """Register all dashboard routes and endpoints."""
300
+ self._register_page_routes()
301
+ self._register_api_routes()
302
+ self._register_websocket_routes()
303
+
304
+ def _register_page_routes(self) -> None:
305
+ """Register HTML page routes."""
306
+ self.app.get("/", response_class=HTMLResponse, tags=["Dashboard"])(self._route_home)
307
+ self.app.get("/sessions", response_class=HTMLResponse, tags=["Dashboard"])(
308
+ self._route_sessions
309
+ )
310
+ self.app.get("/session/{session_id}", response_class=HTMLResponse, tags=["Dashboard"])(
311
+ self._route_session_detail
312
+ )
313
+ self.app.get("/protocols", response_class=HTMLResponse, tags=["Dashboard"])(
314
+ self._route_protocols
315
+ )
316
+ self.app.get("/waveforms/{session_id}", response_class=HTMLResponse, tags=["Dashboard"])(
317
+ self._route_waveforms
318
+ )
319
+ self.app.get("/reports/{session_id}", response_class=HTMLResponse, tags=["Dashboard"])(
320
+ self._route_reports
321
+ )
322
+ self.app.get("/export/{session_id}", response_class=HTMLResponse, tags=["Dashboard"])(
323
+ self._route_export
324
+ )
325
+
326
+ def _base_context(self, request: Request, **kwargs: Any) -> dict[str, Any]:
327
+ """Build base template context."""
328
+ return {
329
+ "request": request,
330
+ "title": self.config.title,
331
+ "theme": self.config.theme,
332
+ **kwargs,
333
+ }
334
+
335
+ async def _route_home(self, request: Request) -> HTMLResponse:
336
+ """Dashboard home page with file upload."""
337
+ return self.templates.TemplateResponse(
338
+ "home.html",
339
+ self._base_context(
340
+ request, max_file_size_mb=self.config.max_file_size // (1024 * 1024)
341
+ ),
342
+ )
343
+
344
+ async def _route_sessions(self, request: Request) -> HTMLResponse:
345
+ """Sessions management page."""
346
+ return self.templates.TemplateResponse(
347
+ "sessions.html",
348
+ self._base_context(request, sessions=self.api_server.session_manager.list_sessions()),
349
+ )
350
+
351
+ async def _route_session_detail(self, request: Request, session_id: str) -> HTMLResponse:
352
+ """Session detail page with analysis results."""
353
+ session = self._get_session_or_404(session_id)
354
+ return self.templates.TemplateResponse(
355
+ "session_detail.html",
356
+ self._base_context(
357
+ request, session=session, protocol_spec=self._extract_protocol_spec(session)
358
+ ),
359
+ )
360
+
361
+ async def _route_protocols(self, request: Request) -> HTMLResponse:
362
+ """Protocols browser page."""
363
+ return self.templates.TemplateResponse(
364
+ "protocols.html", self._base_context(request, protocols=self._gather_protocol_list())
365
+ )
366
+
367
+ async def _route_waveforms(self, request: Request, session_id: str) -> HTMLResponse:
368
+ """Interactive waveform viewer page."""
369
+ session = self._get_session_or_404(session_id)
370
+ return self.templates.TemplateResponse(
371
+ "waveforms.html",
372
+ self._base_context(
373
+ request,
374
+ session_id=session_id,
375
+ filename=session["filename"],
376
+ plotly_config=json.dumps(self.config.plotly_config),
377
+ ),
378
+ )
379
+
380
+ async def _route_reports(self, request: Request, session_id: str) -> HTMLResponse:
381
+ """Analysis reports page."""
382
+ session = self._get_session_or_404(session_id)
383
+ report_path = self._extract_report_path(session)
384
+ return self.templates.TemplateResponse(
385
+ "reports.html",
386
+ self._base_context(
387
+ request,
388
+ session_id=session_id,
389
+ report_path=str(report_path) if report_path else None,
390
+ ),
391
+ )
392
+
393
+ async def _route_export(self, request: Request, session_id: str) -> HTMLResponse:
394
+ """Export/download page for generated artifacts."""
395
+ session = self._get_session_or_404(session_id)
396
+ return self.templates.TemplateResponse(
397
+ "export.html",
398
+ self._base_context(
399
+ request, session_id=session_id, artifacts=self._extract_artifacts(session)
400
+ ),
401
+ )
402
+
403
+ def _register_api_routes(self) -> None:
404
+ """Register API endpoints for AJAX requests."""
405
+
406
+ @self.app.post("/api/upload", tags=["API"])
407
+ async def upload_file(
408
+ file: UploadFile,
409
+ background_tasks: BackgroundTasks,
410
+ protocol_hint: str | None = None,
411
+ auto_crc: bool = True,
412
+ detect_crypto: bool = True,
413
+ generate_tests: bool = True,
414
+ ) -> JSONResponse:
415
+ """Upload file and start analysis."""
416
+ self._validate_upload_file(file)
417
+ file_data = await self._read_and_validate_file_size(file)
418
+
419
+ options = {
420
+ "protocol_hint": protocol_hint,
421
+ "auto_crc": auto_crc,
422
+ "detect_crypto": detect_crypto,
423
+ "generate_tests": generate_tests,
424
+ }
425
+
426
+ session_id = self._create_analysis_session(file.filename, file_data, options)
427
+
428
+ # Add background task for analysis (FastAPI auto-injects BackgroundTasks)
429
+ background_tasks.add_task(self._run_analysis_with_updates, session_id)
430
+
431
+ return JSONResponse(
432
+ {
433
+ "session_id": session_id,
434
+ "status": "processing",
435
+ "message": "Analysis started",
436
+ "created_at": datetime.utcnow().isoformat(),
437
+ }
438
+ )
439
+
440
+ @self.app.get("/api/session/{session_id}/status", tags=["API"])
441
+ async def get_session_status(session_id: str) -> JSONResponse:
442
+ """Get session status for AJAX polling."""
443
+ session = self._get_session_or_404(session_id)
444
+ return JSONResponse(
445
+ {
446
+ "session_id": session_id,
447
+ "status": session["status"],
448
+ "updated_at": session["updated_at"],
449
+ "error": session.get("error"),
450
+ }
451
+ )
452
+
453
+ @self.app.get("/api/session/{session_id}/waveform", tags=["API"])
454
+ async def get_waveform_data(session_id: str) -> JSONResponse:
455
+ """Get waveform data for Plotly.js visualization."""
456
+ session = self._get_session_or_404(session_id)
457
+ waveform_data = self._generate_waveform_data(session)
458
+ return JSONResponse(waveform_data)
459
+
460
+ @self.app.delete("/api/session/{session_id}", tags=["API"])
461
+ async def delete_session_api(session_id: str) -> JSONResponse:
462
+ """Delete session via AJAX."""
463
+ deleted = self.api_server.session_manager.delete_session(session_id)
464
+ if not deleted:
465
+ raise HTTPException(
466
+ status_code=status.HTTP_404_NOT_FOUND,
467
+ detail=f"Session {session_id} not found",
468
+ )
469
+
470
+ return JSONResponse(
471
+ {
472
+ "message": "Session deleted",
473
+ "session_id": session_id,
474
+ "timestamp": datetime.utcnow().isoformat(),
475
+ }
476
+ )
477
+
478
+ @self.app.get("/api/download/{session_id}/{artifact_type}", tags=["API"])
479
+ async def download_artifact(session_id: str, artifact_type: str) -> FileResponse:
480
+ """Download generated artifact."""
481
+ session = self._get_session_or_404(session_id)
482
+ artifact_path = self._get_artifact_path(session, artifact_type)
483
+ return FileResponse(
484
+ path=str(artifact_path),
485
+ filename=Path(artifact_path).name,
486
+ media_type="application/octet-stream",
487
+ )
488
+
489
+ def _register_websocket_routes(self) -> None:
490
+ """Register WebSocket endpoints for real-time updates."""
491
+
492
+ @self.app.websocket("/ws/{session_id}")
493
+ async def websocket_endpoint(websocket: WebSocket, session_id: str) -> None:
494
+ """WebSocket connection for real-time analysis updates."""
495
+ await self.ws_manager.connect(websocket, session_id)
496
+ try:
497
+ while True:
498
+ data = await websocket.receive_text()
499
+ logger.debug(f"WebSocket received: {data}")
500
+ except WebSocketDisconnect:
501
+ self.ws_manager.disconnect(websocket, session_id)
502
+
503
+ def _get_session_or_404(self, session_id: str) -> dict[str, Any]:
504
+ """Get session or raise 404 error.
505
+
506
+ Args:
507
+ session_id: Session identifier.
508
+
509
+ Returns:
510
+ Session data dictionary.
511
+
512
+ Raises:
513
+ HTTPException: If session not found.
514
+ """
515
+ session = self.api_server.session_manager.get_session(session_id)
516
+ if not session:
517
+ raise HTTPException(
518
+ status_code=status.HTTP_404_NOT_FOUND,
519
+ detail=f"Session {session_id} not found",
520
+ )
521
+ return session
522
+
523
+ def _extract_protocol_spec(self, session: dict[str, Any]) -> dict[str, Any] | None:
524
+ """Extract protocol specification from session result.
525
+
526
+ Args:
527
+ session: Session data dictionary.
528
+
529
+ Returns:
530
+ Serialized protocol spec or None.
531
+ """
532
+ if session["result"]:
533
+ return self.api_server._serialize_protocol_spec(session["result"])
534
+ return None
535
+
536
+ def _extract_report_path(self, session: dict[str, Any]) -> Path | None:
537
+ """Extract report path from session result.
538
+
539
+ Args:
540
+ session: Session data dictionary.
541
+
542
+ Returns:
543
+ Report path or None.
544
+ """
545
+ if session["result"]:
546
+ result = session["result"]
547
+ return getattr(result, "report_path", None)
548
+ return None
549
+
550
+ def _extract_artifacts(self, session: dict[str, Any]) -> dict[str, Any]:
551
+ """Extract artifacts from session result.
552
+
553
+ Args:
554
+ session: Session data dictionary.
555
+
556
+ Returns:
557
+ Dictionary of artifacts.
558
+ """
559
+ if session["result"]:
560
+ return self.api_server._serialize_artifacts(session["result"])
561
+ return {}
562
+
563
+ def _gather_protocol_list(self) -> list[dict[str, Any]]:
564
+ """Gather list of protocols from all sessions.
565
+
566
+ Returns:
567
+ List of protocol information dictionaries.
568
+ """
569
+ protocols = []
570
+ for session in self.api_server.session_manager.sessions.values():
571
+ if session["result"]:
572
+ result = session["result"]
573
+ spec = getattr(result, "protocol_spec", None)
574
+ if spec:
575
+ protocols.append(
576
+ {
577
+ "session_id": session["id"],
578
+ "protocol_name": getattr(spec, "protocol_name", "unknown"),
579
+ "confidence": getattr(result, "confidence_score", 0.0),
580
+ "message_count": len(getattr(spec, "messages", [])),
581
+ "field_count": len(getattr(spec, "fields", [])),
582
+ "filename": session["filename"],
583
+ "created_at": session["created_at"],
584
+ }
585
+ )
586
+ return protocols
587
+
588
+ def _validate_upload_file(self, file: UploadFile) -> None:
589
+ """Validate uploaded file has a filename.
590
+
591
+ Args:
592
+ file: Uploaded file object.
593
+
594
+ Raises:
595
+ HTTPException: If filename is missing.
596
+ """
597
+ if not file.filename:
598
+ raise HTTPException(
599
+ status_code=status.HTTP_400_BAD_REQUEST,
600
+ detail="Filename required",
601
+ )
602
+
603
+ async def _read_and_validate_file_size(self, file: UploadFile) -> bytes:
604
+ """Read and validate file size.
605
+
606
+ Args:
607
+ file: Uploaded file object.
608
+
609
+ Returns:
610
+ File data as bytes.
611
+
612
+ Raises:
613
+ HTTPException: If file exceeds maximum size.
614
+ """
615
+ file_data_raw = await file.read()
616
+ # Ensure we have bytes (UploadFile.read() returns bytes | str based on mode)
617
+ file_data = file_data_raw if isinstance(file_data_raw, bytes) else file_data_raw.encode()
618
+ if len(file_data) > self.config.max_file_size:
619
+ raise HTTPException(
620
+ status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
621
+ detail=f"File too large (max {self.config.max_file_size} bytes)",
622
+ )
623
+ return file_data
624
+
625
+ def _create_analysis_session(
626
+ self, filename: str | None, file_data: bytes, options: dict[str, Any]
627
+ ) -> str:
628
+ """Create analysis session via API server.
629
+
630
+ Args:
631
+ filename: Name of uploaded file.
632
+ file_data: File content as bytes.
633
+ options: Analysis options dictionary.
634
+
635
+ Returns:
636
+ Session ID string.
637
+
638
+ Raises:
639
+ HTTPException: If session creation fails or filename is None.
640
+ """
641
+ if not filename:
642
+ raise HTTPException(
643
+ status_code=status.HTTP_400_BAD_REQUEST,
644
+ detail="Filename required",
645
+ )
646
+ try:
647
+ return self.api_server.session_manager.create_session(filename, file_data, options)
648
+ except RuntimeError as e:
649
+ raise HTTPException(
650
+ status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
651
+ detail=str(e),
652
+ ) from e
653
+
654
+ def _get_artifact_path(self, session: dict[str, Any], artifact_type: str) -> Path:
655
+ """Get path to specific artifact type.
656
+
657
+ Args:
658
+ session: Session data dictionary.
659
+ artifact_type: Type of artifact (dissector, scapy, kaitai, report, tests).
660
+
661
+ Returns:
662
+ Path to artifact file.
663
+
664
+ Raises:
665
+ HTTPException: If artifact type invalid, result missing, or file not found.
666
+ """
667
+ if not session["result"]:
668
+ raise HTTPException(
669
+ status_code=status.HTTP_400_BAD_REQUEST,
670
+ detail="No results available",
671
+ )
672
+
673
+ artifact_map = {
674
+ "dissector": "dissector_path",
675
+ "scapy": "scapy_layer_path",
676
+ "kaitai": "kaitai_path",
677
+ "report": "report_path",
678
+ "tests": "test_vectors_path",
679
+ }
680
+
681
+ if artifact_type not in artifact_map:
682
+ raise HTTPException(
683
+ status_code=status.HTTP_400_BAD_REQUEST,
684
+ detail=f"Invalid artifact type: {artifact_type}",
685
+ )
686
+
687
+ result = session["result"]
688
+ artifact_path = getattr(result, artifact_map[artifact_type], None)
689
+
690
+ if not artifact_path or not Path(artifact_path).exists():
691
+ raise HTTPException(
692
+ status_code=status.HTTP_404_NOT_FOUND,
693
+ detail=f"Artifact not found: {artifact_type}",
694
+ )
695
+
696
+ return Path(artifact_path)
697
+
698
+ async def _run_analysis_with_updates(self, session_id: str) -> None:
699
+ """Run analysis with WebSocket progress updates.
700
+
701
+ Args:
702
+ session_id: Session identifier.
703
+ """
704
+ # Send initial status
705
+ await self.ws_manager.send_message(
706
+ session_id,
707
+ {
708
+ "type": "status",
709
+ "status": "processing",
710
+ "message": "Starting analysis...",
711
+ "progress": 0,
712
+ },
713
+ )
714
+
715
+ # Run actual analysis via API server
716
+ try:
717
+ self.api_server._run_analysis(session_id)
718
+
719
+ # Send completion message
720
+ await self.ws_manager.send_message(
721
+ session_id,
722
+ {
723
+ "type": "status",
724
+ "status": "complete",
725
+ "message": "Analysis complete",
726
+ "progress": 100,
727
+ },
728
+ )
729
+ except Exception as e:
730
+ # Send error message
731
+ await self.ws_manager.send_message(
732
+ session_id,
733
+ {
734
+ "type": "error",
735
+ "status": "error",
736
+ "message": str(e),
737
+ "progress": 0,
738
+ },
739
+ )
740
+
741
+ def _generate_waveform_data(self, session: dict[str, Any]) -> dict[str, Any]:
742
+ """Generate waveform data in Plotly.js format.
743
+
744
+ Args:
745
+ session: Session data.
746
+
747
+ Returns:
748
+ Dict with Plotly.js traces.
749
+ """
750
+ # Simplified waveform generation
751
+ # In production, this would extract actual signal data from the capture
752
+ import numpy as np
753
+
754
+ # Generate sample waveform
755
+ t = np.linspace(0, 1, 1000)
756
+ signal = np.sin(2 * np.pi * 5 * t) + 0.5 * np.sin(2 * np.pi * 15 * t)
757
+
758
+ return {
759
+ "data": [
760
+ {
761
+ "x": t.tolist(),
762
+ "y": signal.tolist(),
763
+ "type": "scatter",
764
+ "mode": "lines",
765
+ "name": "Signal",
766
+ "line": {"color": "#00d9ff", "width": 1},
767
+ }
768
+ ],
769
+ "layout": {
770
+ "title": f"Waveform: {session['filename']}",
771
+ "xaxis": {"title": "Time (s)"},
772
+ "yaxis": {"title": "Amplitude"},
773
+ "template": "plotly_dark" if self.config.theme == "dark" else "plotly_white",
774
+ "hovermode": "x unified",
775
+ },
776
+ }
777
+
778
+ def run(self, reload: bool = False) -> None:
779
+ """Start the web dashboard server.
780
+
781
+ Args:
782
+ reload: Enable auto-reload for development.
783
+ """
784
+ try:
785
+ import uvicorn
786
+ except ImportError as e:
787
+ raise ImportError("uvicorn required. Install with: pip install uvicorn") from e
788
+
789
+ logger.info(f"Starting Oscura Web Dashboard on {self.host}:{self.port}")
790
+ logger.info(f"Dashboard URL: http://{self.host}:{self.port}")
791
+
792
+ uvicorn.run(
793
+ self.app,
794
+ host=self.host,
795
+ port=self.port,
796
+ reload=reload,
797
+ log_level="info",
798
+ )
799
+
800
+
801
+ # ============================================================================
802
+ # Command-Line Interface
803
+ # ============================================================================
804
+
805
+
806
+ def main() -> None:
807
+ """Command-line interface for web dashboard."""
808
+ import argparse
809
+
810
+ parser = argparse.ArgumentParser(
811
+ description="Oscura Web Dashboard",
812
+ formatter_class=argparse.RawDescriptionHelpFormatter,
813
+ )
814
+ parser.add_argument(
815
+ "--host",
816
+ type=str,
817
+ default="127.0.0.1",
818
+ help="Server host address (default: 127.0.0.1)",
819
+ )
820
+ parser.add_argument(
821
+ "--port",
822
+ type=int,
823
+ default=5000,
824
+ help="Server port number (default: 5000)",
825
+ )
826
+ parser.add_argument(
827
+ "--theme",
828
+ type=str,
829
+ choices=["light", "dark"],
830
+ default="dark",
831
+ help="UI theme (default: dark)",
832
+ )
833
+ parser.add_argument(
834
+ "--reload",
835
+ action="store_true",
836
+ help="Enable auto-reload for development",
837
+ )
838
+
839
+ args = parser.parse_args()
840
+
841
+ # Create configuration
842
+ config = DashboardConfig(theme=args.theme)
843
+
844
+ # Create and run dashboard
845
+ dashboard = WebDashboard(host=args.host, port=args.port, config=config)
846
+ dashboard.run(reload=args.reload)
847
+
848
+
849
+ if __name__ == "__main__":
850
+ main()