oscura 0.5.0__py3-none-any.whl → 0.6.0__py3-none-any.whl

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