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,809 @@
1
+ """BLE (Bluetooth Low Energy) protocol analyzer with GATT service discovery.
2
+
3
+ This module provides comprehensive BLE packet analysis including:
4
+ - Advertising packet parsing (ADV_IND, SCAN_RSP, etc.)
5
+ - ATT protocol operation decoding (Read, Write, Notify, etc.)
6
+ - GATT service/characteristic/descriptor discovery
7
+ - Standard and custom UUID mapping
8
+ - Export to JSON/CSV formats
9
+
10
+ Example:
11
+ >>> analyzer = BLEAnalyzer()
12
+ >>> packet = BLEPacket(
13
+ ... timestamp=0.0,
14
+ ... packet_type="ADV_IND",
15
+ ... source_address="AA:BB:CC:DD:EE:FF",
16
+ ... data=adv_data,
17
+ ... )
18
+ >>> analyzer.add_packet(packet)
19
+ >>> services = analyzer.discover_services()
20
+ >>> analyzer.export_services(Path("services.json"))
21
+
22
+ References:
23
+ Bluetooth Core Specification v5.4: https://www.bluetooth.com/specifications/specs/
24
+ GATT Specification Supplement: https://www.bluetooth.com/specifications/specs/
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import csv
30
+ import json
31
+ import struct
32
+ from dataclasses import dataclass, field
33
+ from pathlib import Path
34
+ from typing import Any
35
+
36
+ from oscura.analyzers.protocols.ble.uuids import (
37
+ AD_TYPES,
38
+ get_characteristic_name,
39
+ get_descriptor_name,
40
+ get_service_name,
41
+ uuid_to_string,
42
+ )
43
+
44
+ # BLE Link Layer packet types (PDU Type field in advertising channel)
45
+ BLE_PACKET_TYPES: dict[int, str] = {
46
+ 0x00: "ADV_IND", # Connectable undirected advertising
47
+ 0x01: "ADV_DIRECT_IND", # Connectable directed advertising
48
+ 0x02: "ADV_NONCONN_IND", # Non-connectable undirected advertising
49
+ 0x03: "SCAN_REQ", # Scan request
50
+ 0x04: "SCAN_RSP", # Scan response
51
+ 0x05: "CONNECT_REQ", # Connection request
52
+ 0x06: "ADV_SCAN_IND", # Scannable undirected advertising
53
+ }
54
+
55
+ # ATT Protocol Opcodes
56
+ ATT_OPCODES: dict[int, str] = {
57
+ 0x01: "Error Response",
58
+ 0x02: "Exchange MTU Request",
59
+ 0x03: "Exchange MTU Response",
60
+ 0x04: "Find Information Request",
61
+ 0x05: "Find Information Response",
62
+ 0x06: "Find By Type Value Request",
63
+ 0x07: "Find By Type Value Response",
64
+ 0x08: "Read By Type Request",
65
+ 0x09: "Read By Type Response",
66
+ 0x0A: "Read Request",
67
+ 0x0B: "Read Response",
68
+ 0x0C: "Read Blob Request",
69
+ 0x0D: "Read Blob Response",
70
+ 0x0E: "Read Multiple Request",
71
+ 0x0F: "Read Multiple Response",
72
+ 0x10: "Read By Group Type Request",
73
+ 0x11: "Read By Group Type Response",
74
+ 0x12: "Write Request",
75
+ 0x13: "Write Response",
76
+ 0x16: "Prepare Write Request",
77
+ 0x17: "Prepare Write Response",
78
+ 0x18: "Execute Write Request",
79
+ 0x19: "Execute Write Response",
80
+ 0x1B: "Handle Value Notification",
81
+ 0x1D: "Handle Value Indication",
82
+ 0x1E: "Handle Value Confirmation",
83
+ 0x52: "Write Command",
84
+ 0xD2: "Signed Write Command",
85
+ }
86
+
87
+ # GATT Characteristic properties (bit mask)
88
+ GATT_CHAR_PROPERTIES: dict[int, str] = {
89
+ 0x01: "broadcast",
90
+ 0x02: "read",
91
+ 0x04: "write_no_response",
92
+ 0x08: "write",
93
+ 0x10: "notify",
94
+ 0x20: "indicate",
95
+ 0x40: "authenticated_signed_writes",
96
+ 0x80: "extended_properties",
97
+ }
98
+
99
+
100
+ @dataclass
101
+ class GATTDescriptor:
102
+ """GATT descriptor definition.
103
+
104
+ Attributes:
105
+ uuid: Descriptor UUID (16-bit or 128-bit).
106
+ name: Human-readable name.
107
+ handle: Attribute handle.
108
+ value: Descriptor value (optional).
109
+ """
110
+
111
+ uuid: str
112
+ name: str
113
+ handle: int
114
+ value: bytes | None = None
115
+
116
+ def to_dict(self) -> dict[str, Any]:
117
+ """Convert to dictionary for serialization.
118
+
119
+ Returns:
120
+ Dictionary representation.
121
+ """
122
+ return {
123
+ "uuid": self.uuid,
124
+ "name": self.name,
125
+ "handle": self.handle,
126
+ "value": self.value.hex() if self.value else None,
127
+ }
128
+
129
+
130
+ @dataclass
131
+ class GATTCharacteristic:
132
+ """GATT characteristic definition.
133
+
134
+ Attributes:
135
+ uuid: Characteristic UUID.
136
+ name: Human-readable name.
137
+ properties: List of properties (read, write, notify, etc.).
138
+ handle: Attribute handle.
139
+ value_handle: Value handle (for read/write operations).
140
+ value: Characteristic value (optional).
141
+ descriptors: List of descriptors.
142
+ """
143
+
144
+ uuid: str
145
+ name: str
146
+ properties: list[str]
147
+ handle: int
148
+ value_handle: int | None = None
149
+ value: bytes | None = None
150
+ descriptors: list[GATTDescriptor] = field(default_factory=list)
151
+
152
+ def to_dict(self) -> dict[str, Any]:
153
+ """Convert to dictionary for serialization.
154
+
155
+ Returns:
156
+ Dictionary representation.
157
+ """
158
+ return {
159
+ "uuid": self.uuid,
160
+ "name": self.name,
161
+ "properties": self.properties,
162
+ "handle": self.handle,
163
+ "value_handle": self.value_handle,
164
+ "value": self.value.hex() if self.value else None,
165
+ "descriptors": [d.to_dict() for d in self.descriptors],
166
+ }
167
+
168
+
169
+ @dataclass
170
+ class GATTService:
171
+ """GATT service definition.
172
+
173
+ Attributes:
174
+ uuid: Service UUID.
175
+ name: Human-readable name.
176
+ characteristics: List of characteristics.
177
+ handle_range: (start_handle, end_handle) range.
178
+ """
179
+
180
+ uuid: str
181
+ name: str
182
+ characteristics: list[GATTCharacteristic]
183
+ handle_range: tuple[int, int]
184
+
185
+ def to_dict(self) -> dict[str, Any]:
186
+ """Convert to dictionary for serialization.
187
+
188
+ Returns:
189
+ Dictionary representation.
190
+ """
191
+ return {
192
+ "uuid": self.uuid,
193
+ "name": self.name,
194
+ "handle_range": list(self.handle_range),
195
+ "characteristics": [c.to_dict() for c in self.characteristics],
196
+ }
197
+
198
+
199
+ @dataclass
200
+ class BLEPacket:
201
+ """Represents a BLE packet.
202
+
203
+ Attributes:
204
+ timestamp: Packet timestamp in seconds.
205
+ packet_type: Packet type (e.g., "ADV_IND", "ATT_READ_REQ").
206
+ source_address: Source MAC address (AA:BB:CC:DD:EE:FF).
207
+ dest_address: Destination MAC address (optional).
208
+ rssi: Received Signal Strength Indicator in dBm (optional).
209
+ data: Raw packet data.
210
+ decoded: Decoded packet contents (optional).
211
+ """
212
+
213
+ timestamp: float
214
+ packet_type: str
215
+ source_address: str
216
+ data: bytes
217
+ dest_address: str | None = None
218
+ rssi: int | None = None
219
+ decoded: dict[str, Any] | None = None
220
+
221
+ def to_dict(self) -> dict[str, Any]:
222
+ """Convert to dictionary for serialization.
223
+
224
+ Returns:
225
+ Dictionary representation.
226
+ """
227
+ return {
228
+ "timestamp": self.timestamp,
229
+ "packet_type": self.packet_type,
230
+ "source_address": self.source_address,
231
+ "dest_address": self.dest_address,
232
+ "rssi": self.rssi,
233
+ "data": self.data.hex(),
234
+ "decoded": self.decoded,
235
+ }
236
+
237
+
238
+ def _decode_error_response(data: bytes, result: dict[str, Any]) -> None:
239
+ """Decode ATT Error Response packet.
240
+
241
+ Args:
242
+ data: ATT packet data.
243
+ result: Result dictionary to populate.
244
+ """
245
+ if len(data) >= 5:
246
+ result["request_opcode"] = f"0x{data[1]:02X}"
247
+ result["handle"] = int.from_bytes(data[2:4], "little")
248
+ result["error_code"] = f"0x{data[4]:02X}"
249
+
250
+
251
+ def _decode_mtu_operation(data: bytes, result: dict[str, Any]) -> None:
252
+ """Decode ATT MTU Request/Response.
253
+
254
+ Args:
255
+ data: ATT packet data.
256
+ result: Result dictionary to populate.
257
+ """
258
+ if len(data) >= 3:
259
+ result["mtu"] = int.from_bytes(data[1:3], "little")
260
+
261
+
262
+ def _decode_read_request(data: bytes, result: dict[str, Any]) -> None:
263
+ """Decode ATT Read Request/Blob Request.
264
+
265
+ Args:
266
+ data: ATT packet data.
267
+ result: Result dictionary to populate.
268
+ """
269
+ if len(data) >= 3:
270
+ result["handle"] = int.from_bytes(data[1:3], "little")
271
+
272
+
273
+ def _decode_read_response(data: bytes, result: dict[str, Any]) -> None:
274
+ """Decode ATT Read Response.
275
+
276
+ Args:
277
+ data: ATT packet data.
278
+ result: Result dictionary to populate.
279
+ """
280
+ result["value"] = data[1:].hex()
281
+
282
+
283
+ def _decode_read_by_type_request(data: bytes, result: dict[str, Any]) -> None:
284
+ """Decode ATT Read By Type/Group Type Request.
285
+
286
+ Args:
287
+ data: ATT packet data.
288
+ result: Result dictionary to populate.
289
+ """
290
+ if len(data) >= 7:
291
+ result["start_handle"] = int.from_bytes(data[1:3], "little")
292
+ result["end_handle"] = int.from_bytes(data[3:5], "little")
293
+ uuid_data = data[5:]
294
+ result["uuid"] = uuid_to_string(uuid_data)
295
+
296
+
297
+ def _decode_read_by_type_response(data: bytes, result: dict[str, Any]) -> None:
298
+ """Decode ATT Read By Type/Group Type Response.
299
+
300
+ Args:
301
+ data: ATT packet data.
302
+ result: Result dictionary to populate.
303
+ """
304
+ if len(data) >= 2:
305
+ length = data[1]
306
+ result["attribute_length"] = length
307
+
308
+ attributes = []
309
+ i = 2
310
+ while i + length <= len(data):
311
+ attr_data = data[i : i + length]
312
+ if len(attr_data) >= 2:
313
+ handle = int.from_bytes(attr_data[0:2], "little")
314
+ attributes.append({"handle": handle, "data": attr_data[2:].hex()})
315
+ i += length
316
+ result["attributes"] = attributes
317
+
318
+
319
+ def _decode_write_operation(data: bytes, result: dict[str, Any]) -> None:
320
+ """Decode ATT Write operations (Request/Notification/Indication/Command).
321
+
322
+ Args:
323
+ data: ATT packet data.
324
+ result: Result dictionary to populate.
325
+ """
326
+ if len(data) >= 3:
327
+ result["handle"] = int.from_bytes(data[1:3], "little")
328
+ result["value"] = data[3:].hex()
329
+
330
+
331
+ class BLEAnalyzer:
332
+ """BLE protocol analyzer with GATT service discovery.
333
+
334
+ This analyzer processes BLE packets to extract advertising data,
335
+ decode ATT operations, and discover GATT services/characteristics.
336
+
337
+ Example:
338
+ >>> analyzer = BLEAnalyzer()
339
+ >>> analyzer.register_custom_uuid("0xABCD", "My Custom Service")
340
+ >>> analyzer.add_packet(packet)
341
+ >>> services = analyzer.discover_services()
342
+ >>> print(f"Found {len(services)} services")
343
+ """
344
+
345
+ def __init__(self) -> None:
346
+ """Initialize BLE analyzer."""
347
+ self.packets: list[BLEPacket] = []
348
+ self.services: list[GATTService] = []
349
+ self.custom_uuids: dict[str, str] = {}
350
+ self._service_cache: dict[int, GATTService] = {}
351
+ self._char_cache: dict[int, GATTCharacteristic] = {}
352
+
353
+ def add_packet(self, packet: BLEPacket) -> None:
354
+ """Add BLE packet for analysis.
355
+
356
+ Args:
357
+ packet: BLE packet to add.
358
+
359
+ Example:
360
+ >>> packet = BLEPacket(
361
+ ... timestamp=0.0,
362
+ ... packet_type="ADV_IND",
363
+ ... source_address="AA:BB:CC:DD:EE:FF",
364
+ ... data=b"\\x02\\x01\\x06\\x09\\x09MyDevice",
365
+ ... )
366
+ >>> analyzer.add_packet(packet)
367
+ """
368
+ # Decode packet based on type
369
+ if packet.packet_type.startswith("ADV_") or packet.packet_type == "SCAN_RSP":
370
+ packet.decoded = self.parse_advertising_data(packet.data)
371
+ elif packet.packet_type.startswith("ATT_"):
372
+ packet.decoded = self.decode_att_operation(packet.data)
373
+
374
+ self.packets.append(packet)
375
+
376
+ def parse_advertising_data(self, data: bytes) -> dict[str, Any]:
377
+ """Parse BLE advertising data (AD structures).
378
+
379
+ Args:
380
+ data: Advertising data payload.
381
+
382
+ Returns:
383
+ Dictionary of parsed AD structures.
384
+
385
+ Example:
386
+ >>> data = b"\\x02\\x01\\x06\\x09\\x09MyDevice"
387
+ >>> result = analyzer.parse_advertising_data(data)
388
+ >>> print(result["name"])
389
+ 'MyDevice'
390
+ """
391
+ result: dict[str, Any] = {}
392
+ i = 0
393
+
394
+ while i < len(data):
395
+ if i >= len(data):
396
+ break
397
+
398
+ length = data[i]
399
+ if length == 0 or i + length >= len(data):
400
+ break
401
+
402
+ ad_type = data[i + 1]
403
+ ad_data = data[i + 2 : i + 1 + length]
404
+
405
+ # Parse AD structure by type
406
+ self._parse_ad_structure(ad_type, ad_data, result)
407
+
408
+ i += 1 + length
409
+
410
+ return result
411
+
412
+ def _parse_ad_structure(self, ad_type: int, ad_data: bytes, result: dict[str, Any]) -> None:
413
+ """Parse single AD structure into result dictionary.
414
+
415
+ Args:
416
+ ad_type: AD type code.
417
+ ad_data: AD data bytes.
418
+ result: Result dictionary to update.
419
+ """
420
+ if ad_type == 0x01:
421
+ self._parse_flags(ad_data, result)
422
+ elif ad_type in [0x08, 0x09]:
423
+ result["name"] = ad_data.decode("utf-8", errors="ignore")
424
+ elif ad_type == 0x0A:
425
+ result["tx_power"] = struct.unpack("b", ad_data)[0]
426
+ elif ad_type in [0x02, 0x03]:
427
+ self._parse_service_uuids(ad_data, result)
428
+ elif ad_type == 0x16:
429
+ self._parse_service_data(ad_data, result)
430
+ elif ad_type == 0x19:
431
+ self._parse_appearance(ad_data, result)
432
+ elif ad_type == 0xFF:
433
+ self._parse_manufacturer_data(ad_data, result)
434
+ else:
435
+ ad_type_name = AD_TYPES.get(ad_type, f"Unknown Type 0x{ad_type:02X}")
436
+ result[ad_type_name] = ad_data.hex()
437
+
438
+ def _parse_flags(self, ad_data: bytes, result: dict[str, Any]) -> None:
439
+ """Parse BLE flags AD structure.
440
+
441
+ Args:
442
+ ad_data: Flags data bytes.
443
+ result: Result dictionary to update.
444
+ """
445
+ flags = int.from_bytes(ad_data, "little")
446
+ result["flags"] = {
447
+ "value": flags,
448
+ "le_limited_discoverable": bool(flags & 0x01),
449
+ "le_general_discoverable": bool(flags & 0x02),
450
+ "br_edr_not_supported": bool(flags & 0x04),
451
+ "le_br_edr_controller": bool(flags & 0x08),
452
+ "le_br_edr_host": bool(flags & 0x10),
453
+ }
454
+
455
+ def _parse_service_uuids(self, ad_data: bytes, result: dict[str, Any]) -> None:
456
+ """Parse 16-bit service UUIDs AD structure.
457
+
458
+ Args:
459
+ ad_data: UUID data bytes.
460
+ result: Result dictionary to update.
461
+ """
462
+ uuids = []
463
+ for j in range(0, len(ad_data), 2):
464
+ uuid_val = int.from_bytes(ad_data[j : j + 2], "little")
465
+ uuids.append(f"0x{uuid_val:04X}")
466
+ result["service_uuids"] = uuids
467
+
468
+ def _parse_service_data(self, ad_data: bytes, result: dict[str, Any]) -> None:
469
+ """Parse service data AD structure.
470
+
471
+ Args:
472
+ ad_data: Service data bytes.
473
+ result: Result dictionary to update.
474
+ """
475
+ if len(ad_data) >= 2:
476
+ uuid_val = int.from_bytes(ad_data[0:2], "little")
477
+ service_uuid = f"0x{uuid_val:04X}"
478
+ result["service_data"] = {
479
+ "uuid": service_uuid,
480
+ "data": ad_data[2:].hex(),
481
+ }
482
+
483
+ def _parse_appearance(self, ad_data: bytes, result: dict[str, Any]) -> None:
484
+ """Parse appearance AD structure.
485
+
486
+ Args:
487
+ ad_data: Appearance data bytes.
488
+ result: Result dictionary to update.
489
+ """
490
+ if len(ad_data) >= 2:
491
+ appearance = int.from_bytes(ad_data, "little")
492
+ result["appearance"] = appearance
493
+
494
+ def _parse_manufacturer_data(self, ad_data: bytes, result: dict[str, Any]) -> None:
495
+ """Parse manufacturer data AD structure.
496
+
497
+ Args:
498
+ ad_data: Manufacturer data bytes.
499
+ result: Result dictionary to update.
500
+ """
501
+ if len(ad_data) >= 2:
502
+ company_id = int.from_bytes(ad_data[0:2], "little")
503
+ result["manufacturer_data"] = {
504
+ "company_id": f"0x{company_id:04X}",
505
+ "data": ad_data[2:].hex(),
506
+ }
507
+
508
+ def decode_att_operation(self, data: bytes) -> dict[str, Any]:
509
+ """Decode ATT protocol operation.
510
+
511
+ Args:
512
+ data: ATT packet payload.
513
+
514
+ Returns:
515
+ Dictionary of decoded operation details.
516
+
517
+ Example:
518
+ >>> data = b"\\x0A\\x03\\x00" # Read Request, handle 0x0003
519
+ >>> result = analyzer.decode_att_operation(data)
520
+ >>> print(result["opcode_name"])
521
+ 'Read Request'
522
+ """
523
+ if len(data) < 1:
524
+ return {"error": "Packet too short"}
525
+
526
+ opcode = data[0]
527
+ opcode_name = ATT_OPCODES.get(opcode, f"Unknown Opcode 0x{opcode:02X}")
528
+ result: dict[str, Any] = {
529
+ "opcode": f"0x{opcode:02X}",
530
+ "opcode_name": opcode_name,
531
+ }
532
+
533
+ try:
534
+ # Decode based on opcode category
535
+ if opcode == 0x01:
536
+ _decode_error_response(data, result)
537
+ elif opcode in [0x02, 0x03]:
538
+ _decode_mtu_operation(data, result)
539
+ elif opcode in [0x0A, 0x0C]:
540
+ _decode_read_request(data, result)
541
+ elif opcode == 0x0B:
542
+ _decode_read_response(data, result)
543
+ elif opcode in [0x08, 0x10]:
544
+ _decode_read_by_type_request(data, result)
545
+ elif opcode in [0x09, 0x11]:
546
+ _decode_read_by_type_response(data, result)
547
+ elif opcode in [0x12, 0x1B, 0x1D, 0x52]:
548
+ _decode_write_operation(data, result)
549
+
550
+ except (struct.error, IndexError) as e:
551
+ result["parse_error"] = str(e)
552
+
553
+ return result
554
+
555
+ def discover_services(self) -> list[GATTService]:
556
+ """Discover GATT services from captured ATT packets.
557
+
558
+ Analyzes Read By Group Type responses to build service hierarchy.
559
+
560
+ Returns:
561
+ List of discovered GATT services.
562
+
563
+ Example:
564
+ >>> services = analyzer.discover_services()
565
+ >>> for service in services:
566
+ ... print(f"{service.name}: {service.uuid}")
567
+ """
568
+ self.services.clear()
569
+ self._service_cache.clear()
570
+ self._char_cache.clear()
571
+
572
+ # Find service discovery responses (Read By Group Type Response, UUID 0x2800)
573
+ for packet in self.packets:
574
+ if not packet.decoded:
575
+ continue
576
+
577
+ opcode_name = packet.decoded.get("opcode_name", "")
578
+
579
+ # Service discovery (Primary Service = 0x2800)
580
+ if opcode_name == "Read By Group Type Response":
581
+ attributes = packet.decoded.get("attributes", [])
582
+ for attr in attributes:
583
+ try:
584
+ handle = attr["handle"]
585
+ data_hex = attr["data"]
586
+ data = bytes.fromhex(data_hex)
587
+
588
+ if len(data) >= 4:
589
+ # Format: end_handle (2) + UUID (2 or 16)
590
+ end_handle = int.from_bytes(data[0:2], "little")
591
+ uuid_data = data[2:]
592
+ uuid = uuid_to_string(uuid_data)
593
+
594
+ # Get service name
595
+ service_name = self.get_uuid_name(uuid, "service")
596
+
597
+ service = GATTService(
598
+ uuid=uuid,
599
+ name=service_name,
600
+ characteristics=[],
601
+ handle_range=(handle, end_handle),
602
+ )
603
+ self.services.append(service)
604
+ self._service_cache[handle] = service
605
+ except (ValueError, KeyError):
606
+ continue
607
+
608
+ # Characteristic discovery (Characteristic Declaration = 0x2803)
609
+ elif opcode_name == "Read By Type Response":
610
+ attributes = packet.decoded.get("attributes", [])
611
+ for attr in attributes:
612
+ try:
613
+ handle = attr["handle"]
614
+ data_hex = attr["data"]
615
+ data = bytes.fromhex(data_hex)
616
+
617
+ if len(data) >= 5:
618
+ # Format: properties (1) + value_handle (2) + UUID (2 or 16)
619
+ properties_byte = data[0]
620
+ value_handle = int.from_bytes(data[1:3], "little")
621
+ uuid_data = data[3:]
622
+ uuid = uuid_to_string(uuid_data)
623
+
624
+ # Parse properties
625
+ properties = self._parse_properties(properties_byte)
626
+
627
+ # Get characteristic name
628
+ char_name = self.get_uuid_name(uuid, "characteristic")
629
+
630
+ char = GATTCharacteristic(
631
+ uuid=uuid,
632
+ name=char_name,
633
+ properties=properties,
634
+ handle=handle,
635
+ value_handle=value_handle,
636
+ )
637
+
638
+ # Find parent service
639
+ for service in self.services:
640
+ if service.handle_range[0] <= handle <= service.handle_range[1]:
641
+ service.characteristics.append(char)
642
+ self._char_cache[handle] = char
643
+ break
644
+ except (ValueError, KeyError):
645
+ continue
646
+
647
+ return self.services
648
+
649
+ def _parse_properties(self, properties_byte: int) -> list[str]:
650
+ """Parse GATT characteristic properties byte.
651
+
652
+ Args:
653
+ properties_byte: Properties bit mask.
654
+
655
+ Returns:
656
+ List of property names.
657
+ """
658
+ properties = []
659
+ for bit, name in GATT_CHAR_PROPERTIES.items():
660
+ if properties_byte & bit:
661
+ properties.append(name)
662
+ return properties
663
+
664
+ def register_custom_uuid(self, uuid: str, name: str) -> None:
665
+ """Register custom service/characteristic UUID.
666
+
667
+ Args:
668
+ uuid: UUID string (e.g., "0xABCD" or full format).
669
+ name: Human-readable name.
670
+
671
+ Example:
672
+ >>> analyzer.register_custom_uuid("0xABCD", "My Custom Service")
673
+ """
674
+ self.custom_uuids[uuid.upper()] = name
675
+
676
+ def get_uuid_name(self, uuid: str, uuid_type: str = "service") -> str:
677
+ """Get name for UUID (checks custom mappings first).
678
+
679
+ Args:
680
+ uuid: UUID string.
681
+ uuid_type: Type of UUID ("service", "characteristic", "descriptor").
682
+
683
+ Returns:
684
+ Human-readable name.
685
+ """
686
+ # Check custom mappings first
687
+ if uuid.upper() in self.custom_uuids:
688
+ return self.custom_uuids[uuid.upper()]
689
+
690
+ # Check standard mappings
691
+ if uuid_type == "service":
692
+ return get_service_name(uuid)
693
+ elif uuid_type == "characteristic":
694
+ return get_characteristic_name(uuid)
695
+ elif uuid_type == "descriptor":
696
+ return get_descriptor_name(uuid)
697
+ else:
698
+ return f"Unknown {uuid_type}"
699
+
700
+ def export_services(self, output_path: Path, format: str = "json") -> None:
701
+ """Export discovered services to file.
702
+
703
+ Args:
704
+ output_path: Output file path.
705
+ format: Export format ("json" or "csv").
706
+
707
+ Raises:
708
+ ValueError: If format is not supported.
709
+
710
+ Example:
711
+ >>> analyzer.export_services(Path("services.json"))
712
+ >>> analyzer.export_services(Path("services.csv"), format="csv")
713
+ """
714
+ if format == "json":
715
+ self._export_json(output_path)
716
+ elif format == "csv":
717
+ self._export_csv(output_path)
718
+ else:
719
+ raise ValueError(f"Unsupported format: {format}")
720
+
721
+ def _export_json(self, output_path: Path) -> None:
722
+ """Export services as JSON.
723
+
724
+ Args:
725
+ output_path: Output file path.
726
+ """
727
+ data = {
728
+ "services": [service.to_dict() for service in self.services],
729
+ "packet_count": len(self.packets),
730
+ }
731
+ with output_path.open("w") as f:
732
+ json.dump(data, f, indent=2)
733
+
734
+ def _export_csv(self, output_path: Path) -> None:
735
+ """Export services as CSV.
736
+
737
+ Args:
738
+ output_path: Output file path.
739
+ """
740
+ with output_path.open("w", newline="") as f:
741
+ writer = csv.writer(f)
742
+ writer.writerow(
743
+ [
744
+ "Service UUID",
745
+ "Service Name",
746
+ "Handle Range",
747
+ "Characteristic UUID",
748
+ "Characteristic Name",
749
+ "Properties",
750
+ ]
751
+ )
752
+
753
+ for service in self.services:
754
+ if not service.characteristics:
755
+ writer.writerow(
756
+ [
757
+ service.uuid,
758
+ service.name,
759
+ f"{service.handle_range[0]}-{service.handle_range[1]}",
760
+ "",
761
+ "",
762
+ "",
763
+ ]
764
+ )
765
+ else:
766
+ for char in service.characteristics:
767
+ writer.writerow(
768
+ [
769
+ service.uuid,
770
+ service.name,
771
+ f"{service.handle_range[0]}-{service.handle_range[1]}",
772
+ char.uuid,
773
+ char.name,
774
+ ", ".join(char.properties),
775
+ ]
776
+ )
777
+
778
+ def get_statistics(self) -> dict[str, Any]:
779
+ """Get analysis statistics.
780
+
781
+ Returns:
782
+ Dictionary of statistics.
783
+
784
+ Example:
785
+ >>> stats = analyzer.get_statistics()
786
+ >>> print(f"Total packets: {stats['total_packets']}")
787
+ """
788
+ packet_types: dict[str, int] = {}
789
+ for packet in self.packets:
790
+ packet_types[packet.packet_type] = packet_types.get(packet.packet_type, 0) + 1
791
+
792
+ return {
793
+ "total_packets": len(self.packets),
794
+ "packet_types": packet_types,
795
+ "services_discovered": len(self.services),
796
+ "total_characteristics": sum(len(s.characteristics) for s in self.services),
797
+ }
798
+
799
+
800
+ __all__ = [
801
+ "ATT_OPCODES",
802
+ "BLE_PACKET_TYPES",
803
+ "GATT_CHAR_PROPERTIES",
804
+ "BLEAnalyzer",
805
+ "BLEPacket",
806
+ "GATTCharacteristic",
807
+ "GATTDescriptor",
808
+ "GATTService",
809
+ ]