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,921 @@
1
+ """REST API server for Oscura protocol analysis.
2
+
3
+ This module provides a RESTful API interface for accessing Oscura functionality
4
+ remotely, enabling web dashboards, automation, and integration with other tools.
5
+
6
+ Supports:
7
+ - File upload and analysis
8
+ - Session management
9
+ - Protocol discovery
10
+ - Export to multiple formats (Wireshark, Scapy, Kaitai)
11
+ - OpenAPI documentation
12
+
13
+ Example:
14
+ >>> from oscura.api.rest_server import RESTAPIServer
15
+ >>> server = RESTAPIServer(host="0.0.0.0", port=8000)
16
+ >>> server.run() # Starts server on http://0.0.0.0:8000
17
+ >>> # API docs at http://0.0.0.0:8000/docs
18
+
19
+ Architecture:
20
+ - FastAPI framework (with Flask fallback if unavailable)
21
+ - Async request processing for large files
22
+ - CORS support for web clients
23
+ - Rate limiting for API protection
24
+ - Authentication support (API keys)
25
+ - OpenAPI/Swagger documentation
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import hashlib
31
+ import logging
32
+ import tempfile
33
+ import time
34
+ import uuid
35
+ from dataclasses import dataclass, field
36
+ from datetime import datetime
37
+ from pathlib import Path
38
+ from typing import TYPE_CHECKING, Any
39
+
40
+ if TYPE_CHECKING:
41
+ from oscura.workflows.complete_re import CompleteREResult
42
+
43
+ logger = logging.getLogger(__name__)
44
+
45
+ # Try to import FastAPI, fallback to Flask if unavailable
46
+ try:
47
+ from fastapi import (
48
+ BackgroundTasks,
49
+ Depends,
50
+ FastAPI,
51
+ HTTPException,
52
+ Security,
53
+ UploadFile,
54
+ status,
55
+ )
56
+ from fastapi.middleware.cors import CORSMiddleware
57
+ from fastapi.security import (
58
+ HTTPAuthorizationCredentials,
59
+ HTTPBearer,
60
+ )
61
+
62
+ HAS_FASTAPI = True
63
+ except ImportError:
64
+ HAS_FASTAPI = False
65
+ logger.warning("FastAPI not available. Install with: pip install 'fastapi[all]' uvicorn")
66
+
67
+
68
+ # ============================================================================
69
+ # Request/Response Models
70
+ # ============================================================================
71
+
72
+
73
+ @dataclass
74
+ class AnalysisRequest:
75
+ """Request model for protocol analysis.
76
+
77
+ Attributes:
78
+ file_data: Uploaded file bytes.
79
+ filename: Original filename.
80
+ protocol_hint: Optional protocol type hint (uart, spi, i2c, can).
81
+ auto_crc: Enable automatic CRC recovery.
82
+ detect_crypto: Enable cryptographic field detection.
83
+ generate_tests: Generate test vectors.
84
+ export_formats: Formats to export (wireshark, scapy, kaitai).
85
+ """
86
+
87
+ file_data: bytes
88
+ filename: str
89
+ protocol_hint: str | None = None
90
+ auto_crc: bool = True
91
+ detect_crypto: bool = True
92
+ generate_tests: bool = True
93
+ export_formats: list[str] = field(default_factory=lambda: ["wireshark"])
94
+
95
+
96
+ @dataclass
97
+ class AnalysisResponse:
98
+ """Response model for analysis request.
99
+
100
+ Attributes:
101
+ session_id: Unique session identifier.
102
+ status: Analysis status (processing, complete, error).
103
+ protocols_found: List of detected protocols.
104
+ confidence_scores: Confidence scores per protocol (0.0-1.0).
105
+ message: Human-readable status message.
106
+ created_at: Timestamp of analysis start.
107
+ estimated_duration: Estimated completion time (seconds).
108
+ """
109
+
110
+ session_id: str
111
+ status: str
112
+ protocols_found: list[str] = field(default_factory=list)
113
+ confidence_scores: dict[str, float] = field(default_factory=dict)
114
+ message: str = ""
115
+ created_at: str = ""
116
+ estimated_duration: float = 0.0
117
+
118
+
119
+ @dataclass
120
+ class SessionResponse:
121
+ """Response model for session details.
122
+
123
+ Attributes:
124
+ session_id: Unique session identifier.
125
+ status: Current session status.
126
+ protocol_spec: Inferred protocol specification.
127
+ messages_decoded: Number of messages decoded.
128
+ fields_discovered: Number of fields discovered.
129
+ artifacts: Dict of generated artifacts (paths).
130
+ statistics: Analysis statistics.
131
+ created_at: Session creation timestamp.
132
+ updated_at: Last update timestamp.
133
+ """
134
+
135
+ session_id: str
136
+ status: str
137
+ protocol_spec: dict[str, Any] | None = None
138
+ messages_decoded: int = 0
139
+ fields_discovered: int = 0
140
+ artifacts: dict[str, str] = field(default_factory=dict)
141
+ statistics: dict[str, Any] = field(default_factory=dict)
142
+ created_at: str = ""
143
+ updated_at: str = ""
144
+
145
+
146
+ @dataclass
147
+ class ProtocolResponse:
148
+ """Response model for protocol details.
149
+
150
+ Attributes:
151
+ protocol_name: Detected protocol name.
152
+ confidence: Detection confidence (0.0-1.0).
153
+ message_count: Number of messages.
154
+ field_count: Number of fields.
155
+ fields: List of field specifications.
156
+ state_machine: State machine if extracted.
157
+ crc_info: CRC parameters if recovered.
158
+ """
159
+
160
+ protocol_name: str
161
+ confidence: float
162
+ message_count: int
163
+ field_count: int
164
+ fields: list[dict[str, Any]] = field(default_factory=list)
165
+ state_machine: dict[str, Any] | None = None
166
+ crc_info: dict[str, Any] | None = None
167
+
168
+
169
+ @dataclass
170
+ class ErrorResponse:
171
+ """Response model for errors.
172
+
173
+ Attributes:
174
+ error_code: Error code identifier.
175
+ message: Human-readable error message.
176
+ details: Additional error details.
177
+ timestamp: Error timestamp.
178
+ """
179
+
180
+ error_code: str
181
+ message: str
182
+ details: dict[str, Any] = field(default_factory=dict)
183
+ timestamp: str = field(default_factory=lambda: datetime.utcnow().isoformat())
184
+
185
+
186
+ # ============================================================================
187
+ # Session Management
188
+ # ============================================================================
189
+
190
+
191
+ class SessionManager:
192
+ """Manages active analysis sessions.
193
+
194
+ Attributes:
195
+ sessions: Dict of session_id -> session data.
196
+ max_sessions: Maximum concurrent sessions.
197
+ session_timeout: Session timeout in seconds (default 1 hour).
198
+ """
199
+
200
+ def __init__(self, max_sessions: int = 100, session_timeout: float = 3600.0):
201
+ """Initialize session manager.
202
+
203
+ Args:
204
+ max_sessions: Maximum concurrent sessions.
205
+ session_timeout: Session timeout in seconds.
206
+ """
207
+ self.sessions: dict[str, dict[str, Any]] = {}
208
+ self.max_sessions = max_sessions
209
+ self.session_timeout = session_timeout
210
+
211
+ def create_session(self, filename: str, file_data: bytes, options: dict[str, Any]) -> str:
212
+ """Create a new analysis session.
213
+
214
+ Args:
215
+ filename: Uploaded filename.
216
+ file_data: File content bytes.
217
+ options: Analysis options.
218
+
219
+ Returns:
220
+ Unique session ID.
221
+
222
+ Raises:
223
+ RuntimeError: If max sessions exceeded.
224
+ """
225
+ if len(self.sessions) >= self.max_sessions:
226
+ # Clean up old sessions
227
+ self._cleanup_old_sessions()
228
+
229
+ if len(self.sessions) >= self.max_sessions:
230
+ raise RuntimeError(f"Maximum sessions ({self.max_sessions}) exceeded")
231
+
232
+ session_id = str(uuid.uuid4())
233
+ self.sessions[session_id] = {
234
+ "id": session_id,
235
+ "filename": filename,
236
+ "file_data": file_data,
237
+ "file_hash": hashlib.sha256(file_data).hexdigest(),
238
+ "options": options,
239
+ "status": "created",
240
+ "result": None,
241
+ "error": None,
242
+ "created_at": datetime.utcnow().isoformat(),
243
+ "updated_at": datetime.utcnow().isoformat(),
244
+ "accessed_at": time.time(),
245
+ }
246
+ return session_id
247
+
248
+ def get_session(self, session_id: str) -> dict[str, Any] | None:
249
+ """Get session by ID.
250
+
251
+ Args:
252
+ session_id: Session identifier.
253
+
254
+ Returns:
255
+ Session data or None if not found.
256
+ """
257
+ if session_id in self.sessions:
258
+ self.sessions[session_id]["accessed_at"] = time.time()
259
+ return self.sessions[session_id]
260
+ return None
261
+
262
+ def update_session(
263
+ self, session_id: str, status: str, result: Any = None, error: str | None = None
264
+ ) -> None:
265
+ """Update session status.
266
+
267
+ Args:
268
+ session_id: Session identifier.
269
+ status: New status.
270
+ result: Analysis result.
271
+ error: Error message if failed.
272
+ """
273
+ if session_id in self.sessions:
274
+ self.sessions[session_id]["status"] = status
275
+ self.sessions[session_id]["result"] = result
276
+ self.sessions[session_id]["error"] = error
277
+ self.sessions[session_id]["updated_at"] = datetime.utcnow().isoformat()
278
+ self.sessions[session_id]["accessed_at"] = time.time()
279
+
280
+ def delete_session(self, session_id: str) -> bool:
281
+ """Delete a session.
282
+
283
+ Args:
284
+ session_id: Session identifier.
285
+
286
+ Returns:
287
+ True if deleted, False if not found.
288
+ """
289
+ if session_id in self.sessions:
290
+ del self.sessions[session_id]
291
+ return True
292
+ return False
293
+
294
+ def list_sessions(self) -> list[dict[str, Any]]:
295
+ """List all active sessions.
296
+
297
+ Returns:
298
+ List of session summaries.
299
+ """
300
+ return [
301
+ {
302
+ "session_id": sid,
303
+ "status": data["status"],
304
+ "filename": data["filename"],
305
+ "created_at": data["created_at"],
306
+ "updated_at": data["updated_at"],
307
+ }
308
+ for sid, data in self.sessions.items()
309
+ ]
310
+
311
+ def _cleanup_old_sessions(self) -> None:
312
+ """Remove sessions that have exceeded timeout."""
313
+ current_time = time.time()
314
+ to_delete = [
315
+ sid
316
+ for sid, data in self.sessions.items()
317
+ if current_time - data["accessed_at"] > self.session_timeout
318
+ ]
319
+ for sid in to_delete:
320
+ logger.info(f"Cleaning up timed-out session: {sid}")
321
+ self.delete_session(sid)
322
+
323
+
324
+ # ============================================================================
325
+ # REST API Server (FastAPI)
326
+ # ============================================================================
327
+
328
+
329
+ class RESTAPIServer:
330
+ """REST API server for Oscura.
331
+
332
+ Provides HTTP endpoints for protocol analysis, session management,
333
+ and artifact export.
334
+
335
+ Security Warning:
336
+ Default CORS configuration allows all origins (["*"]) for development
337
+ convenience. For production deployments, explicitly configure allowed
338
+ origins to prevent CSRF attacks:
339
+
340
+ Example (Production):
341
+ server = RESTAPIServer(
342
+ api_key="your-secret-key", # Always set in production
343
+ enable_cors=True,
344
+ cors_origins=["https://trusted-domain.com"]
345
+ )
346
+
347
+ Never deploy to production with:
348
+ - cors_origins=["*"]
349
+ - No api_key configured
350
+ - Exposed to public internet without reverse proxy
351
+
352
+ Example:
353
+ >>> server = RESTAPIServer(host="0.0.0.0", port=8000)
354
+ >>> server.run() # Starts server
355
+ >>> # Visit http://0.0.0.0:8000/docs for API documentation
356
+ """
357
+
358
+ def __init__(
359
+ self,
360
+ host: str = "127.0.0.1",
361
+ port: int = 8000,
362
+ max_sessions: int = 100,
363
+ enable_cors: bool = True,
364
+ cors_origins: list[str] | None = None,
365
+ api_key: str | None = None,
366
+ rate_limit: int | None = None,
367
+ ):
368
+ """Initialize REST API server.
369
+
370
+ Args:
371
+ host: Server host address.
372
+ port: Server port number.
373
+ max_sessions: Maximum concurrent sessions.
374
+ enable_cors: Enable CORS middleware.
375
+ cors_origins: Allowed CORS origins (default: all).
376
+ api_key: Optional API key for authentication.
377
+ rate_limit: Optional rate limit (requests per minute).
378
+
379
+ Raises:
380
+ ImportError: If FastAPI is not available.
381
+ """
382
+ if not HAS_FASTAPI:
383
+ raise ImportError("FastAPI required. Install with: pip install 'fastapi[all]' uvicorn")
384
+
385
+ self.host = host
386
+ self.port = port
387
+ self.api_key = api_key
388
+ self.rate_limit = rate_limit
389
+
390
+ # Initialize session manager
391
+ self.session_manager = SessionManager(max_sessions=max_sessions)
392
+
393
+ # Initialize security
394
+ self._security = HTTPBearer(auto_error=False) if HAS_FASTAPI else None
395
+
396
+ # Create FastAPI app with dynamic version from package metadata (SSOT: pyproject.toml)
397
+ try:
398
+ from importlib.metadata import version
399
+
400
+ app_version = version("oscura")
401
+ except Exception:
402
+ app_version = "0.0.0+dev"
403
+
404
+ self.app = FastAPI(
405
+ title="Oscura REST API",
406
+ description="Hardware reverse engineering and protocol analysis API",
407
+ version=app_version,
408
+ docs_url="/docs",
409
+ redoc_url="/redoc",
410
+ openapi_url="/api/openapi.json",
411
+ )
412
+
413
+ # Add CORS middleware
414
+ if enable_cors:
415
+ origins = cors_origins or ["*"]
416
+ self.app.add_middleware(
417
+ CORSMiddleware,
418
+ allow_origins=origins,
419
+ allow_credentials=True,
420
+ allow_methods=["*"],
421
+ allow_headers=["*"],
422
+ )
423
+
424
+ # Register routes
425
+ self._register_routes()
426
+
427
+ def _create_auth_dependency(self) -> Any:
428
+ """Create authentication dependency for route protection.
429
+
430
+ Returns:
431
+ FastAPI dependency that validates API key.
432
+
433
+ Security:
434
+ Implements Bearer token authentication (SEC-002 fix).
435
+ If api_key is None, all requests are allowed (development mode).
436
+ If api_key is set, requests MUST include valid Bearer token.
437
+ """
438
+
439
+ async def verify_api_key(
440
+ credentials: HTTPAuthorizationCredentials | None = Security(self._security), # noqa: B008
441
+ ) -> None:
442
+ """Verify API key if authentication is configured."""
443
+ if not self.api_key:
444
+ return # No auth required if not configured
445
+
446
+ if not credentials or credentials.credentials != self.api_key:
447
+ raise HTTPException(
448
+ status_code=status.HTTP_401_UNAUTHORIZED,
449
+ detail="Invalid or missing API key",
450
+ headers={"WWW-Authenticate": "Bearer"},
451
+ )
452
+
453
+ return Depends(verify_api_key)
454
+
455
+ def _register_routes(self) -> None:
456
+ """Register API endpoints."""
457
+ self._register_health_route()
458
+ self._register_analyze_route()
459
+ self._register_sessions_routes()
460
+ self._register_protocols_route()
461
+ self._register_export_route()
462
+
463
+ def _register_health_route(self) -> None:
464
+ """Register health check endpoint."""
465
+
466
+ @self.app.get("/api/health", tags=["Health"])
467
+ async def health_check() -> dict[str, Any]:
468
+ """Health check endpoint with dynamic version from package metadata."""
469
+ try:
470
+ from importlib.metadata import version
471
+
472
+ current_version = version("oscura")
473
+ except Exception:
474
+ current_version = "0.0.0+dev"
475
+
476
+ return {
477
+ "status": "healthy",
478
+ "version": current_version,
479
+ "sessions_active": len(self.session_manager.sessions),
480
+ "timestamp": datetime.utcnow().isoformat(),
481
+ }
482
+
483
+ def _register_analyze_route(self) -> None:
484
+ """Register analysis endpoint."""
485
+
486
+ @self.app.post(
487
+ "/api/v1/analyze",
488
+ tags=["Analysis"],
489
+ status_code=status.HTTP_202_ACCEPTED,
490
+ dependencies=[self._create_auth_dependency()],
491
+ )
492
+ async def analyze(
493
+ file: UploadFile,
494
+ background_tasks: BackgroundTasks,
495
+ protocol_hint: str | None = None,
496
+ auto_crc: bool = True,
497
+ detect_crypto: bool = True,
498
+ generate_tests: bool = True,
499
+ ) -> dict[str, Any]:
500
+ """Analyze uploaded file for protocol discovery."""
501
+ if not file.filename:
502
+ raise HTTPException(
503
+ status_code=status.HTTP_400_BAD_REQUEST, detail="Filename required"
504
+ )
505
+
506
+ file_data = await file.read()
507
+ options = {
508
+ "protocol_hint": protocol_hint,
509
+ "auto_crc": auto_crc,
510
+ "detect_crypto": detect_crypto,
511
+ "generate_tests": generate_tests,
512
+ }
513
+
514
+ try:
515
+ session_id = self.session_manager.create_session(file.filename, file_data, options)
516
+ except RuntimeError as e:
517
+ raise HTTPException(
518
+ status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail=str(e)
519
+ ) from e
520
+
521
+ # FastAPI automatically injects BackgroundTasks
522
+ background_tasks.add_task(self._run_analysis, session_id)
523
+
524
+ return {
525
+ "session_id": session_id,
526
+ "status": "processing",
527
+ "message": "Analysis started",
528
+ "created_at": datetime.utcnow().isoformat(),
529
+ "estimated_duration": 30.0,
530
+ }
531
+
532
+ def _register_sessions_routes(self) -> None:
533
+ """Register session management endpoints."""
534
+
535
+ @self.app.get(
536
+ "/api/v1/sessions",
537
+ tags=["Sessions"],
538
+ dependencies=[self._create_auth_dependency()],
539
+ )
540
+ async def list_sessions() -> dict[str, Any]:
541
+ """List all active sessions."""
542
+ sessions = self.session_manager.list_sessions()
543
+ return {
544
+ "sessions": sessions,
545
+ "count": len(sessions),
546
+ "timestamp": datetime.utcnow().isoformat(),
547
+ }
548
+
549
+ @self.app.get(
550
+ "/api/v1/sessions/{session_id}",
551
+ tags=["Sessions"],
552
+ dependencies=[self._create_auth_dependency()],
553
+ )
554
+ async def get_session(session_id: str) -> dict[str, Any]:
555
+ """Get session details."""
556
+ session = self.session_manager.get_session(session_id)
557
+ if not session:
558
+ raise HTTPException(
559
+ status_code=status.HTTP_404_NOT_FOUND, detail=f"Session {session_id} not found"
560
+ )
561
+
562
+ return self._build_session_response(session)
563
+
564
+ @self.app.delete(
565
+ "/api/v1/sessions/{session_id}",
566
+ tags=["Sessions"],
567
+ dependencies=[self._create_auth_dependency()],
568
+ )
569
+ async def delete_session(session_id: str) -> dict[str, Any]:
570
+ """Delete a session."""
571
+ deleted = self.session_manager.delete_session(session_id)
572
+ if not deleted:
573
+ raise HTTPException(
574
+ status_code=status.HTTP_404_NOT_FOUND, detail=f"Session {session_id} not found"
575
+ )
576
+
577
+ return {
578
+ "message": "Session deleted",
579
+ "session_id": session_id,
580
+ "timestamp": datetime.utcnow().isoformat(),
581
+ }
582
+
583
+ def _register_protocols_route(self) -> None:
584
+ """Register protocols listing endpoint."""
585
+
586
+ @self.app.get(
587
+ "/api/v1/protocols",
588
+ tags=["Protocols"],
589
+ dependencies=[self._create_auth_dependency()],
590
+ )
591
+ async def list_protocols() -> dict[str, Any]:
592
+ """List all discovered protocols across sessions."""
593
+ protocols = self._extract_protocols_from_sessions()
594
+ return {
595
+ "protocols": protocols,
596
+ "count": len(protocols),
597
+ "timestamp": datetime.utcnow().isoformat(),
598
+ }
599
+
600
+ def _register_export_route(self) -> None:
601
+ """Register export endpoint."""
602
+
603
+ @self.app.post(
604
+ "/api/v1/export/{export_format}",
605
+ tags=["Export"],
606
+ dependencies=[self._create_auth_dependency()],
607
+ )
608
+ async def export_results(session_id: str, export_format: str) -> dict[str, Any]:
609
+ """Export analysis results in specified format."""
610
+ session = self._validate_session_for_export(session_id, export_format)
611
+ artifacts = self._serialize_artifacts(session["result"])
612
+ artifact_path = self._get_export_artifact_path(export_format, artifacts)
613
+
614
+ return {
615
+ "format": export_format,
616
+ "file_path": artifact_path,
617
+ "session_id": session_id,
618
+ "timestamp": datetime.utcnow().isoformat(),
619
+ }
620
+
621
+ def _build_session_response(self, session: dict[str, Any]) -> dict[str, Any]:
622
+ """Build session response dict.
623
+
624
+ Args:
625
+ session: Session data from manager.
626
+
627
+ Returns:
628
+ Response dict with session details.
629
+ """
630
+ response = {
631
+ "session_id": session["id"],
632
+ "status": session["status"],
633
+ "filename": session["filename"],
634
+ "file_hash": session["file_hash"],
635
+ "created_at": session["created_at"],
636
+ "updated_at": session["updated_at"],
637
+ }
638
+
639
+ if session["result"]:
640
+ result = session["result"]
641
+ response["protocol_spec"] = self._serialize_protocol_spec(result)
642
+ response["confidence_score"] = getattr(result, "confidence_score", 0.0)
643
+ response["artifacts"] = self._serialize_artifacts(result)
644
+
645
+ if session["error"]:
646
+ response["error"] = session["error"]
647
+
648
+ return response
649
+
650
+ def _extract_protocols_from_sessions(self) -> list[dict[str, Any]]:
651
+ """Extract protocol information from all sessions.
652
+
653
+ Returns:
654
+ List of protocol dicts.
655
+ """
656
+ protocols = []
657
+ for session in self.session_manager.sessions.values():
658
+ if session["result"]:
659
+ result = session["result"]
660
+ spec = getattr(result, "protocol_spec", None)
661
+ if spec:
662
+ protocols.append(
663
+ {
664
+ "session_id": session["id"],
665
+ "protocol_name": getattr(spec, "protocol_name", "unknown"),
666
+ "confidence": getattr(result, "confidence_score", 0.0),
667
+ "message_count": len(getattr(spec, "messages", [])),
668
+ "field_count": len(getattr(spec, "fields", [])),
669
+ }
670
+ )
671
+ return protocols
672
+
673
+ def _validate_session_for_export(self, session_id: str, export_format: str) -> dict[str, Any]:
674
+ """Validate session exists and is ready for export.
675
+
676
+ Args:
677
+ session_id: Session identifier.
678
+ export_format: Export format.
679
+
680
+ Returns:
681
+ Session data.
682
+
683
+ Raises:
684
+ HTTPException: If validation fails.
685
+ """
686
+ session = self.session_manager.get_session(session_id)
687
+ if not session:
688
+ raise HTTPException(
689
+ status_code=status.HTTP_404_NOT_FOUND, detail=f"Session {session_id} not found"
690
+ )
691
+
692
+ if session["status"] != "complete":
693
+ raise HTTPException(
694
+ status_code=status.HTTP_400_BAD_REQUEST,
695
+ detail=f"Session {session_id} not complete (status: {session['status']})",
696
+ )
697
+
698
+ valid_formats = ["wireshark", "scapy", "kaitai"]
699
+ if export_format not in valid_formats:
700
+ raise HTTPException(
701
+ status_code=status.HTTP_400_BAD_REQUEST,
702
+ detail=f"Invalid format. Must be one of: {valid_formats}",
703
+ )
704
+
705
+ return session
706
+
707
+ def _get_export_artifact_path(self, export_format: str, artifacts: dict[str, str]) -> str:
708
+ """Get artifact path for export format.
709
+
710
+ Args:
711
+ export_format: Export format name.
712
+ artifacts: Artifacts dict from serialization.
713
+
714
+ Returns:
715
+ Artifact file path.
716
+
717
+ Raises:
718
+ HTTPException: If artifact not available.
719
+ """
720
+ format_map = {
721
+ "wireshark": "dissector_path",
722
+ "scapy": "scapy_layer_path",
723
+ "kaitai": "kaitai_path",
724
+ }
725
+
726
+ artifact_key = format_map.get(export_format)
727
+ if not artifact_key or artifact_key not in artifacts:
728
+ raise HTTPException(
729
+ status_code=status.HTTP_404_NOT_FOUND,
730
+ detail=f"No {export_format} artifact available",
731
+ )
732
+
733
+ return artifacts[artifact_key]
734
+
735
+ def _run_analysis(self, session_id: str) -> None:
736
+ """Run protocol analysis in background.
737
+
738
+ Args:
739
+ session_id: Session identifier.
740
+ """
741
+ session = self.session_manager.get_session(session_id)
742
+ if not session:
743
+ logger.error(f"Session {session_id} not found for analysis")
744
+ return
745
+
746
+ self.session_manager.update_session(session_id, "processing")
747
+
748
+ try:
749
+ # Import here to avoid circular imports
750
+ from oscura.workflows.complete_re import full_protocol_re
751
+
752
+ # Save uploaded file to temp location
753
+ with tempfile.NamedTemporaryFile(
754
+ delete=False, suffix=Path(session["filename"]).suffix
755
+ ) as tmp_file:
756
+ tmp_file.write(session["file_data"])
757
+ tmp_path = tmp_file.name
758
+
759
+ # Run analysis
760
+ result = full_protocol_re(
761
+ captures=tmp_path,
762
+ protocol_hint=session["options"].get("protocol_hint"),
763
+ auto_crc=session["options"].get("auto_crc", True),
764
+ detect_crypto=session["options"].get("detect_crypto", True),
765
+ generate_tests=session["options"].get("generate_tests", True),
766
+ )
767
+
768
+ # Clean up temp file
769
+ Path(tmp_path).unlink(missing_ok=True)
770
+
771
+ # Update session with results
772
+ self.session_manager.update_session(session_id, "complete", result=result)
773
+ logger.info(f"Analysis complete for session {session_id}")
774
+
775
+ except Exception as e:
776
+ logger.exception(f"Analysis failed for session {session_id}: {e}")
777
+ self.session_manager.update_session(session_id, "error", error=str(e))
778
+
779
+ def _serialize_protocol_spec(self, result: CompleteREResult) -> dict[str, Any]:
780
+ """Serialize protocol specification to dict.
781
+
782
+ Args:
783
+ result: Complete RE result.
784
+
785
+ Returns:
786
+ Serialized protocol spec.
787
+ """
788
+ spec = result.protocol_spec
789
+ messages = getattr(spec, "messages", [])
790
+ fields = getattr(spec, "fields", [])
791
+
792
+ # Handle Mock objects and other non-list types in tests
793
+ try:
794
+ message_count = len(messages) if messages else 0
795
+ except TypeError:
796
+ message_count = 0
797
+
798
+ try:
799
+ field_count = len(fields) if fields else 0
800
+ except TypeError:
801
+ field_count = 0
802
+ fields = []
803
+
804
+ return {
805
+ "protocol_name": getattr(spec, "protocol_name", "unknown"),
806
+ "message_count": message_count,
807
+ "field_count": field_count,
808
+ "fields": [
809
+ {
810
+ "name": getattr(f, "name", ""),
811
+ "offset": getattr(f, "offset", 0),
812
+ "length": getattr(f, "length", 0),
813
+ "type": getattr(f, "field_type", ""),
814
+ "confidence": getattr(f, "confidence", 0.0),
815
+ }
816
+ for f in fields
817
+ ],
818
+ }
819
+
820
+ def _serialize_artifacts(self, result: CompleteREResult) -> dict[str, str]:
821
+ """Serialize artifact paths to dict.
822
+
823
+ Args:
824
+ result: Complete RE result.
825
+
826
+ Returns:
827
+ Dict of artifact type to path.
828
+ """
829
+ artifacts = {}
830
+ if result.dissector_path:
831
+ artifacts["dissector_path"] = str(result.dissector_path)
832
+ if result.scapy_layer_path:
833
+ artifacts["scapy_layer_path"] = str(result.scapy_layer_path)
834
+ if result.kaitai_path:
835
+ artifacts["kaitai_path"] = str(result.kaitai_path)
836
+ if result.test_vectors_path:
837
+ artifacts["test_vectors_path"] = str(result.test_vectors_path)
838
+ if result.report_path:
839
+ artifacts["report_path"] = str(result.report_path)
840
+ return artifacts
841
+
842
+ def run(self, reload: bool = False) -> None:
843
+ """Start the REST API server.
844
+
845
+ Args:
846
+ reload: Enable auto-reload for development.
847
+ """
848
+ try:
849
+ import uvicorn
850
+ except ImportError as e:
851
+ raise ImportError("uvicorn required. Install with: pip install uvicorn") from e
852
+
853
+ logger.info(f"Starting Oscura REST API server on {self.host}:{self.port}")
854
+ logger.info(f"API documentation: http://{self.host}:{self.port}/docs")
855
+
856
+ uvicorn.run(
857
+ self.app,
858
+ host=self.host,
859
+ port=self.port,
860
+ reload=reload,
861
+ log_level="info",
862
+ )
863
+
864
+
865
+ # ============================================================================
866
+ # Command-Line Interface
867
+ # ============================================================================
868
+
869
+
870
+ def main() -> None:
871
+ """Command-line interface for REST API server."""
872
+ import argparse
873
+
874
+ parser = argparse.ArgumentParser(
875
+ description="Oscura REST API Server",
876
+ formatter_class=argparse.RawDescriptionHelpFormatter,
877
+ )
878
+ parser.add_argument(
879
+ "--host",
880
+ type=str,
881
+ default="127.0.0.1",
882
+ help="Server host address (default: 127.0.0.1)",
883
+ )
884
+ parser.add_argument(
885
+ "--port",
886
+ type=int,
887
+ default=8000,
888
+ help="Server port number (default: 8000)",
889
+ )
890
+ parser.add_argument(
891
+ "--max-sessions",
892
+ type=int,
893
+ default=100,
894
+ help="Maximum concurrent sessions (default: 100)",
895
+ )
896
+ parser.add_argument(
897
+ "--reload",
898
+ action="store_true",
899
+ help="Enable auto-reload for development",
900
+ )
901
+ parser.add_argument(
902
+ "--no-cors",
903
+ action="store_true",
904
+ help="Disable CORS middleware",
905
+ )
906
+
907
+ args = parser.parse_args()
908
+
909
+ # Create and run server
910
+ server = RESTAPIServer(
911
+ host=args.host,
912
+ port=args.port,
913
+ max_sessions=args.max_sessions,
914
+ enable_cors=not args.no_cors,
915
+ )
916
+
917
+ server.run(reload=args.reload)
918
+
919
+
920
+ if __name__ == "__main__":
921
+ main()