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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (513) hide show
  1. oscura/__init__.py +169 -167
  2. oscura/analyzers/__init__.py +3 -0
  3. oscura/analyzers/classification.py +659 -0
  4. oscura/analyzers/digital/__init__.py +0 -48
  5. oscura/analyzers/digital/edges.py +325 -65
  6. oscura/analyzers/digital/extraction.py +0 -195
  7. oscura/analyzers/digital/quality.py +293 -166
  8. oscura/analyzers/digital/timing.py +260 -115
  9. oscura/analyzers/digital/timing_numba.py +334 -0
  10. oscura/analyzers/entropy.py +605 -0
  11. oscura/analyzers/eye/diagram.py +176 -109
  12. oscura/analyzers/eye/metrics.py +5 -5
  13. oscura/analyzers/jitter/__init__.py +6 -4
  14. oscura/analyzers/jitter/ber.py +52 -52
  15. oscura/analyzers/jitter/classification.py +156 -0
  16. oscura/analyzers/jitter/decomposition.py +163 -113
  17. oscura/analyzers/jitter/spectrum.py +80 -64
  18. oscura/analyzers/ml/__init__.py +39 -0
  19. oscura/analyzers/ml/features.py +600 -0
  20. oscura/analyzers/ml/signal_classifier.py +604 -0
  21. oscura/analyzers/packet/daq.py +246 -158
  22. oscura/analyzers/packet/parser.py +12 -1
  23. oscura/analyzers/packet/payload.py +50 -2110
  24. oscura/analyzers/packet/payload_analysis.py +361 -181
  25. oscura/analyzers/packet/payload_patterns.py +133 -70
  26. oscura/analyzers/packet/stream.py +84 -23
  27. oscura/analyzers/patterns/__init__.py +26 -5
  28. oscura/analyzers/patterns/anomaly_detection.py +908 -0
  29. oscura/analyzers/patterns/clustering.py +169 -108
  30. oscura/analyzers/patterns/clustering_optimized.py +227 -0
  31. oscura/analyzers/patterns/discovery.py +1 -1
  32. oscura/analyzers/patterns/matching.py +581 -197
  33. oscura/analyzers/patterns/pattern_mining.py +778 -0
  34. oscura/analyzers/patterns/periodic.py +121 -38
  35. oscura/analyzers/patterns/sequences.py +175 -78
  36. oscura/analyzers/power/conduction.py +1 -1
  37. oscura/analyzers/power/soa.py +6 -6
  38. oscura/analyzers/power/switching.py +250 -110
  39. oscura/analyzers/protocol/__init__.py +17 -1
  40. oscura/analyzers/protocols/__init__.py +1 -22
  41. oscura/analyzers/protocols/base.py +6 -6
  42. oscura/analyzers/protocols/ble/__init__.py +38 -0
  43. oscura/analyzers/protocols/ble/analyzer.py +809 -0
  44. oscura/analyzers/protocols/ble/uuids.py +288 -0
  45. oscura/analyzers/protocols/can.py +257 -127
  46. oscura/analyzers/protocols/can_fd.py +107 -80
  47. oscura/analyzers/protocols/flexray.py +139 -80
  48. oscura/analyzers/protocols/hdlc.py +93 -58
  49. oscura/analyzers/protocols/i2c.py +247 -106
  50. oscura/analyzers/protocols/i2s.py +138 -86
  51. oscura/analyzers/protocols/industrial/__init__.py +40 -0
  52. oscura/analyzers/protocols/industrial/bacnet/__init__.py +33 -0
  53. oscura/analyzers/protocols/industrial/bacnet/analyzer.py +708 -0
  54. oscura/analyzers/protocols/industrial/bacnet/encoding.py +412 -0
  55. oscura/analyzers/protocols/industrial/bacnet/services.py +622 -0
  56. oscura/analyzers/protocols/industrial/ethercat/__init__.py +30 -0
  57. oscura/analyzers/protocols/industrial/ethercat/analyzer.py +474 -0
  58. oscura/analyzers/protocols/industrial/ethercat/mailbox.py +339 -0
  59. oscura/analyzers/protocols/industrial/ethercat/topology.py +166 -0
  60. oscura/analyzers/protocols/industrial/modbus/__init__.py +31 -0
  61. oscura/analyzers/protocols/industrial/modbus/analyzer.py +525 -0
  62. oscura/analyzers/protocols/industrial/modbus/crc.py +79 -0
  63. oscura/analyzers/protocols/industrial/modbus/functions.py +436 -0
  64. oscura/analyzers/protocols/industrial/opcua/__init__.py +21 -0
  65. oscura/analyzers/protocols/industrial/opcua/analyzer.py +552 -0
  66. oscura/analyzers/protocols/industrial/opcua/datatypes.py +446 -0
  67. oscura/analyzers/protocols/industrial/opcua/services.py +264 -0
  68. oscura/analyzers/protocols/industrial/profinet/__init__.py +23 -0
  69. oscura/analyzers/protocols/industrial/profinet/analyzer.py +441 -0
  70. oscura/analyzers/protocols/industrial/profinet/dcp.py +263 -0
  71. oscura/analyzers/protocols/industrial/profinet/ptcp.py +200 -0
  72. oscura/analyzers/protocols/jtag.py +180 -98
  73. oscura/analyzers/protocols/lin.py +219 -114
  74. oscura/analyzers/protocols/manchester.py +4 -4
  75. oscura/analyzers/protocols/onewire.py +253 -149
  76. oscura/analyzers/protocols/parallel_bus/__init__.py +20 -0
  77. oscura/analyzers/protocols/parallel_bus/centronics.py +92 -0
  78. oscura/analyzers/protocols/parallel_bus/gpib.py +137 -0
  79. oscura/analyzers/protocols/spi.py +192 -95
  80. oscura/analyzers/protocols/swd.py +321 -167
  81. oscura/analyzers/protocols/uart.py +267 -125
  82. oscura/analyzers/protocols/usb.py +235 -131
  83. oscura/analyzers/side_channel/power.py +17 -12
  84. oscura/analyzers/signal/__init__.py +15 -0
  85. oscura/analyzers/signal/timing_analysis.py +1086 -0
  86. oscura/analyzers/signal_integrity/__init__.py +4 -1
  87. oscura/analyzers/signal_integrity/sparams.py +2 -19
  88. oscura/analyzers/spectral/chunked.py +129 -60
  89. oscura/analyzers/spectral/chunked_fft.py +300 -94
  90. oscura/analyzers/spectral/chunked_wavelet.py +100 -80
  91. oscura/analyzers/statistical/checksum.py +376 -217
  92. oscura/analyzers/statistical/classification.py +229 -107
  93. oscura/analyzers/statistical/entropy.py +78 -53
  94. oscura/analyzers/statistics/correlation.py +407 -211
  95. oscura/analyzers/statistics/outliers.py +2 -2
  96. oscura/analyzers/statistics/streaming.py +30 -5
  97. oscura/analyzers/validation.py +216 -101
  98. oscura/analyzers/waveform/measurements.py +9 -0
  99. oscura/analyzers/waveform/measurements_with_uncertainty.py +31 -15
  100. oscura/analyzers/waveform/spectral.py +500 -228
  101. oscura/api/__init__.py +31 -5
  102. oscura/api/dsl/__init__.py +582 -0
  103. oscura/{dsl → api/dsl}/commands.py +43 -76
  104. oscura/{dsl → api/dsl}/interpreter.py +26 -51
  105. oscura/{dsl → api/dsl}/parser.py +107 -77
  106. oscura/{dsl → api/dsl}/repl.py +2 -2
  107. oscura/api/dsl.py +1 -1
  108. oscura/{integrations → api/integrations}/__init__.py +1 -1
  109. oscura/{integrations → api/integrations}/llm.py +201 -102
  110. oscura/api/operators.py +3 -3
  111. oscura/api/optimization.py +144 -30
  112. oscura/api/rest_server.py +921 -0
  113. oscura/api/server/__init__.py +17 -0
  114. oscura/api/server/dashboard.py +850 -0
  115. oscura/api/server/static/README.md +34 -0
  116. oscura/api/server/templates/base.html +181 -0
  117. oscura/api/server/templates/export.html +120 -0
  118. oscura/api/server/templates/home.html +284 -0
  119. oscura/api/server/templates/protocols.html +58 -0
  120. oscura/api/server/templates/reports.html +43 -0
  121. oscura/api/server/templates/session_detail.html +89 -0
  122. oscura/api/server/templates/sessions.html +83 -0
  123. oscura/api/server/templates/waveforms.html +73 -0
  124. oscura/automotive/__init__.py +8 -1
  125. oscura/automotive/can/__init__.py +10 -0
  126. oscura/automotive/can/checksum.py +3 -1
  127. oscura/automotive/can/dbc_generator.py +590 -0
  128. oscura/automotive/can/message_wrapper.py +121 -74
  129. oscura/automotive/can/patterns.py +98 -21
  130. oscura/automotive/can/session.py +292 -56
  131. oscura/automotive/can/state_machine.py +6 -3
  132. oscura/automotive/can/stimulus_response.py +97 -75
  133. oscura/automotive/dbc/__init__.py +10 -2
  134. oscura/automotive/dbc/generator.py +84 -56
  135. oscura/automotive/dbc/parser.py +6 -6
  136. oscura/automotive/dtc/data.json +2763 -0
  137. oscura/automotive/dtc/database.py +2 -2
  138. oscura/automotive/flexray/__init__.py +31 -0
  139. oscura/automotive/flexray/analyzer.py +504 -0
  140. oscura/automotive/flexray/crc.py +185 -0
  141. oscura/automotive/flexray/fibex.py +449 -0
  142. oscura/automotive/j1939/__init__.py +45 -8
  143. oscura/automotive/j1939/analyzer.py +605 -0
  144. oscura/automotive/j1939/spns.py +326 -0
  145. oscura/automotive/j1939/transport.py +306 -0
  146. oscura/automotive/lin/__init__.py +47 -0
  147. oscura/automotive/lin/analyzer.py +612 -0
  148. oscura/automotive/loaders/blf.py +13 -2
  149. oscura/automotive/loaders/csv_can.py +143 -72
  150. oscura/automotive/loaders/dispatcher.py +50 -2
  151. oscura/automotive/loaders/mdf.py +86 -45
  152. oscura/automotive/loaders/pcap.py +111 -61
  153. oscura/automotive/uds/__init__.py +4 -0
  154. oscura/automotive/uds/analyzer.py +725 -0
  155. oscura/automotive/uds/decoder.py +140 -58
  156. oscura/automotive/uds/models.py +7 -1
  157. oscura/automotive/visualization.py +1 -1
  158. oscura/cli/analyze.py +348 -0
  159. oscura/cli/batch.py +142 -122
  160. oscura/cli/benchmark.py +275 -0
  161. oscura/cli/characterize.py +137 -82
  162. oscura/cli/compare.py +224 -131
  163. oscura/cli/completion.py +250 -0
  164. oscura/cli/config_cmd.py +361 -0
  165. oscura/cli/decode.py +164 -87
  166. oscura/cli/export.py +286 -0
  167. oscura/cli/main.py +115 -31
  168. oscura/{onboarding → cli/onboarding}/__init__.py +3 -3
  169. oscura/{onboarding → cli/onboarding}/help.py +80 -58
  170. oscura/{onboarding → cli/onboarding}/tutorials.py +97 -72
  171. oscura/{onboarding → cli/onboarding}/wizard.py +55 -36
  172. oscura/cli/progress.py +147 -0
  173. oscura/cli/shell.py +157 -135
  174. oscura/cli/validate_cmd.py +204 -0
  175. oscura/cli/visualize.py +158 -0
  176. oscura/convenience.py +125 -79
  177. oscura/core/__init__.py +4 -2
  178. oscura/core/backend_selector.py +3 -3
  179. oscura/core/cache.py +126 -15
  180. oscura/core/cancellation.py +1 -1
  181. oscura/{config → core/config}/__init__.py +20 -11
  182. oscura/{config → core/config}/defaults.py +1 -1
  183. oscura/{config → core/config}/loader.py +7 -5
  184. oscura/{config → core/config}/memory.py +5 -5
  185. oscura/{config → core/config}/migration.py +1 -1
  186. oscura/{config → core/config}/pipeline.py +99 -23
  187. oscura/{config → core/config}/preferences.py +1 -1
  188. oscura/{config → core/config}/protocol.py +3 -3
  189. oscura/{config → core/config}/schema.py +426 -272
  190. oscura/{config → core/config}/settings.py +1 -1
  191. oscura/{config → core/config}/thresholds.py +195 -153
  192. oscura/core/correlation.py +5 -6
  193. oscura/core/cross_domain.py +0 -2
  194. oscura/core/debug.py +9 -5
  195. oscura/{extensibility → core/extensibility}/docs.py +158 -70
  196. oscura/{extensibility → core/extensibility}/extensions.py +160 -76
  197. oscura/{extensibility → core/extensibility}/logging.py +1 -1
  198. oscura/{extensibility → core/extensibility}/measurements.py +1 -1
  199. oscura/{extensibility → core/extensibility}/plugins.py +1 -1
  200. oscura/{extensibility → core/extensibility}/templates.py +73 -3
  201. oscura/{extensibility → core/extensibility}/validation.py +1 -1
  202. oscura/core/gpu_backend.py +11 -7
  203. oscura/core/log_query.py +101 -11
  204. oscura/core/logging.py +126 -54
  205. oscura/core/logging_advanced.py +5 -5
  206. oscura/core/memory_limits.py +108 -70
  207. oscura/core/memory_monitor.py +2 -2
  208. oscura/core/memory_progress.py +7 -7
  209. oscura/core/memory_warnings.py +1 -1
  210. oscura/core/numba_backend.py +13 -13
  211. oscura/{plugins → core/plugins}/__init__.py +9 -9
  212. oscura/{plugins → core/plugins}/base.py +7 -7
  213. oscura/{plugins → core/plugins}/cli.py +3 -3
  214. oscura/{plugins → core/plugins}/discovery.py +186 -106
  215. oscura/{plugins → core/plugins}/lifecycle.py +1 -1
  216. oscura/{plugins → core/plugins}/manager.py +7 -7
  217. oscura/{plugins → core/plugins}/registry.py +3 -3
  218. oscura/{plugins → core/plugins}/versioning.py +1 -1
  219. oscura/core/progress.py +16 -1
  220. oscura/core/provenance.py +8 -2
  221. oscura/{schemas → core/schemas}/__init__.py +2 -2
  222. oscura/core/schemas/bus_configuration.json +322 -0
  223. oscura/core/schemas/device_mapping.json +182 -0
  224. oscura/core/schemas/packet_format.json +418 -0
  225. oscura/core/schemas/protocol_definition.json +363 -0
  226. oscura/core/types.py +4 -0
  227. oscura/core/uncertainty.py +3 -3
  228. oscura/correlation/__init__.py +52 -0
  229. oscura/correlation/multi_protocol.py +811 -0
  230. oscura/discovery/auto_decoder.py +117 -35
  231. oscura/discovery/comparison.py +191 -86
  232. oscura/discovery/quality_validator.py +155 -68
  233. oscura/discovery/signal_detector.py +196 -79
  234. oscura/export/__init__.py +18 -20
  235. oscura/export/kaitai_struct.py +513 -0
  236. oscura/export/scapy_layer.py +801 -0
  237. oscura/export/wireshark/README.md +15 -15
  238. oscura/export/wireshark/generator.py +1 -1
  239. oscura/export/wireshark/templates/dissector.lua.j2 +2 -2
  240. oscura/export/wireshark_dissector.py +746 -0
  241. oscura/guidance/wizard.py +207 -111
  242. oscura/hardware/__init__.py +19 -0
  243. oscura/{acquisition → hardware/acquisition}/__init__.py +4 -4
  244. oscura/{acquisition → hardware/acquisition}/file.py +2 -2
  245. oscura/{acquisition → hardware/acquisition}/hardware.py +7 -7
  246. oscura/{acquisition → hardware/acquisition}/saleae.py +15 -12
  247. oscura/{acquisition → hardware/acquisition}/socketcan.py +1 -1
  248. oscura/{acquisition → hardware/acquisition}/streaming.py +2 -2
  249. oscura/{acquisition → hardware/acquisition}/synthetic.py +3 -3
  250. oscura/{acquisition → hardware/acquisition}/visa.py +33 -11
  251. oscura/hardware/firmware/__init__.py +29 -0
  252. oscura/hardware/firmware/pattern_recognition.py +874 -0
  253. oscura/hardware/hal_detector.py +736 -0
  254. oscura/hardware/security/__init__.py +37 -0
  255. oscura/hardware/security/side_channel_detector.py +1126 -0
  256. oscura/inference/__init__.py +4 -0
  257. oscura/inference/active_learning/README.md +7 -7
  258. oscura/inference/active_learning/observation_table.py +4 -1
  259. oscura/inference/alignment.py +216 -123
  260. oscura/inference/bayesian.py +113 -33
  261. oscura/inference/crc_reverse.py +101 -55
  262. oscura/inference/logic.py +6 -2
  263. oscura/inference/message_format.py +342 -183
  264. oscura/inference/protocol.py +95 -44
  265. oscura/inference/protocol_dsl.py +180 -82
  266. oscura/inference/signal_intelligence.py +1439 -706
  267. oscura/inference/spectral.py +99 -57
  268. oscura/inference/state_machine.py +810 -158
  269. oscura/inference/stream.py +270 -110
  270. oscura/iot/__init__.py +34 -0
  271. oscura/iot/coap/__init__.py +32 -0
  272. oscura/iot/coap/analyzer.py +668 -0
  273. oscura/iot/coap/options.py +212 -0
  274. oscura/iot/lorawan/__init__.py +21 -0
  275. oscura/iot/lorawan/crypto.py +206 -0
  276. oscura/iot/lorawan/decoder.py +801 -0
  277. oscura/iot/lorawan/mac_commands.py +341 -0
  278. oscura/iot/mqtt/__init__.py +27 -0
  279. oscura/iot/mqtt/analyzer.py +999 -0
  280. oscura/iot/mqtt/properties.py +315 -0
  281. oscura/iot/zigbee/__init__.py +31 -0
  282. oscura/iot/zigbee/analyzer.py +615 -0
  283. oscura/iot/zigbee/security.py +153 -0
  284. oscura/iot/zigbee/zcl.py +349 -0
  285. oscura/jupyter/display.py +125 -45
  286. oscura/{exploratory → jupyter/exploratory}/__init__.py +8 -8
  287. oscura/{exploratory → jupyter/exploratory}/error_recovery.py +298 -141
  288. oscura/jupyter/exploratory/fuzzy.py +746 -0
  289. oscura/{exploratory → jupyter/exploratory}/fuzzy_advanced.py +258 -100
  290. oscura/{exploratory → jupyter/exploratory}/legacy.py +464 -242
  291. oscura/{exploratory → jupyter/exploratory}/parse.py +167 -145
  292. oscura/{exploratory → jupyter/exploratory}/recovery.py +119 -87
  293. oscura/jupyter/exploratory/sync.py +612 -0
  294. oscura/{exploratory → jupyter/exploratory}/unknown.py +299 -176
  295. oscura/jupyter/magic.py +4 -4
  296. oscura/{ui → jupyter/ui}/__init__.py +2 -2
  297. oscura/{ui → jupyter/ui}/formatters.py +3 -3
  298. oscura/{ui → jupyter/ui}/progressive_display.py +153 -82
  299. oscura/loaders/__init__.py +171 -63
  300. oscura/loaders/binary.py +88 -1
  301. oscura/loaders/chipwhisperer.py +153 -137
  302. oscura/loaders/configurable.py +208 -86
  303. oscura/loaders/csv_loader.py +458 -215
  304. oscura/loaders/hdf5_loader.py +278 -119
  305. oscura/loaders/lazy.py +87 -54
  306. oscura/loaders/mmap_loader.py +1 -1
  307. oscura/loaders/numpy_loader.py +253 -116
  308. oscura/loaders/pcap.py +226 -151
  309. oscura/loaders/rigol.py +110 -49
  310. oscura/loaders/sigrok.py +201 -78
  311. oscura/loaders/tdms.py +81 -58
  312. oscura/loaders/tektronix.py +291 -174
  313. oscura/loaders/touchstone.py +182 -87
  314. oscura/loaders/vcd.py +215 -117
  315. oscura/loaders/wav.py +155 -68
  316. oscura/reporting/__init__.py +9 -7
  317. oscura/reporting/analyze.py +352 -146
  318. oscura/reporting/argument_preparer.py +69 -14
  319. oscura/reporting/auto_report.py +97 -61
  320. oscura/reporting/batch.py +131 -58
  321. oscura/reporting/chart_selection.py +57 -45
  322. oscura/reporting/comparison.py +63 -17
  323. oscura/reporting/content/executive.py +76 -24
  324. oscura/reporting/core_formats/multi_format.py +11 -8
  325. oscura/reporting/engine.py +312 -158
  326. oscura/reporting/enhanced_reports.py +949 -0
  327. oscura/reporting/export.py +86 -43
  328. oscura/reporting/formatting/numbers.py +69 -42
  329. oscura/reporting/html.py +139 -58
  330. oscura/reporting/index.py +137 -65
  331. oscura/reporting/output.py +158 -67
  332. oscura/reporting/pdf.py +67 -102
  333. oscura/reporting/plots.py +191 -112
  334. oscura/reporting/sections.py +88 -47
  335. oscura/reporting/standards.py +104 -61
  336. oscura/reporting/summary_generator.py +75 -55
  337. oscura/reporting/tables.py +138 -54
  338. oscura/reporting/templates/enhanced/protocol_re.html +525 -0
  339. oscura/reporting/templates/index.md +13 -13
  340. oscura/sessions/__init__.py +14 -23
  341. oscura/sessions/base.py +3 -3
  342. oscura/sessions/blackbox.py +106 -10
  343. oscura/sessions/generic.py +2 -2
  344. oscura/sessions/legacy.py +783 -0
  345. oscura/side_channel/__init__.py +63 -0
  346. oscura/side_channel/dpa.py +1025 -0
  347. oscura/utils/__init__.py +15 -1
  348. oscura/utils/autodetect.py +1 -5
  349. oscura/utils/bitwise.py +118 -0
  350. oscura/{builders → utils/builders}/__init__.py +1 -1
  351. oscura/{comparison → utils/comparison}/__init__.py +6 -6
  352. oscura/{comparison → utils/comparison}/compare.py +202 -101
  353. oscura/{comparison → utils/comparison}/golden.py +83 -63
  354. oscura/{comparison → utils/comparison}/limits.py +313 -89
  355. oscura/{comparison → utils/comparison}/mask.py +151 -45
  356. oscura/{comparison → utils/comparison}/trace_diff.py +1 -1
  357. oscura/{comparison → utils/comparison}/visualization.py +147 -89
  358. oscura/{component → utils/component}/__init__.py +3 -3
  359. oscura/{component → utils/component}/impedance.py +122 -58
  360. oscura/{component → utils/component}/reactive.py +165 -168
  361. oscura/{component → utils/component}/transmission_line.py +3 -3
  362. oscura/{filtering → utils/filtering}/__init__.py +6 -6
  363. oscura/{filtering → utils/filtering}/base.py +1 -1
  364. oscura/{filtering → utils/filtering}/convenience.py +2 -2
  365. oscura/{filtering → utils/filtering}/design.py +169 -93
  366. oscura/{filtering → utils/filtering}/filters.py +2 -2
  367. oscura/{filtering → utils/filtering}/introspection.py +2 -2
  368. oscura/utils/geometry.py +31 -0
  369. oscura/utils/imports.py +184 -0
  370. oscura/utils/lazy.py +1 -1
  371. oscura/{math → utils/math}/__init__.py +2 -2
  372. oscura/{math → utils/math}/arithmetic.py +114 -48
  373. oscura/{math → utils/math}/interpolation.py +139 -106
  374. oscura/utils/memory.py +129 -66
  375. oscura/utils/memory_advanced.py +92 -9
  376. oscura/utils/memory_extensions.py +10 -8
  377. oscura/{optimization → utils/optimization}/__init__.py +1 -1
  378. oscura/{optimization → utils/optimization}/search.py +2 -2
  379. oscura/utils/performance/__init__.py +58 -0
  380. oscura/utils/performance/caching.py +889 -0
  381. oscura/utils/performance/lsh_clustering.py +333 -0
  382. oscura/utils/performance/memory_optimizer.py +699 -0
  383. oscura/utils/performance/optimizations.py +675 -0
  384. oscura/utils/performance/parallel.py +654 -0
  385. oscura/utils/performance/profiling.py +661 -0
  386. oscura/{pipeline → utils/pipeline}/base.py +1 -1
  387. oscura/{pipeline → utils/pipeline}/composition.py +11 -3
  388. oscura/{pipeline → utils/pipeline}/parallel.py +3 -2
  389. oscura/{pipeline → utils/pipeline}/pipeline.py +1 -1
  390. oscura/{pipeline → utils/pipeline}/reverse_engineering.py +412 -221
  391. oscura/{search → utils/search}/__init__.py +3 -3
  392. oscura/{search → utils/search}/anomaly.py +188 -58
  393. oscura/utils/search/context.py +294 -0
  394. oscura/{search → utils/search}/pattern.py +138 -10
  395. oscura/utils/serial.py +51 -0
  396. oscura/utils/storage/__init__.py +61 -0
  397. oscura/utils/storage/database.py +1166 -0
  398. oscura/{streaming → utils/streaming}/chunked.py +302 -143
  399. oscura/{streaming → utils/streaming}/progressive.py +1 -1
  400. oscura/{streaming → utils/streaming}/realtime.py +3 -2
  401. oscura/{triggering → utils/triggering}/__init__.py +6 -6
  402. oscura/{triggering → utils/triggering}/base.py +6 -6
  403. oscura/{triggering → utils/triggering}/edge.py +2 -2
  404. oscura/{triggering → utils/triggering}/pattern.py +2 -2
  405. oscura/{triggering → utils/triggering}/pulse.py +115 -74
  406. oscura/{triggering → utils/triggering}/window.py +2 -2
  407. oscura/utils/validation.py +32 -0
  408. oscura/validation/__init__.py +121 -0
  409. oscura/{compliance → validation/compliance}/__init__.py +5 -5
  410. oscura/{compliance → validation/compliance}/advanced.py +5 -5
  411. oscura/{compliance → validation/compliance}/masks.py +1 -1
  412. oscura/{compliance → validation/compliance}/reporting.py +127 -53
  413. oscura/{compliance → validation/compliance}/testing.py +114 -52
  414. oscura/validation/compliance_tests.py +915 -0
  415. oscura/validation/fuzzer.py +990 -0
  416. oscura/validation/grammar_tests.py +596 -0
  417. oscura/validation/grammar_validator.py +904 -0
  418. oscura/validation/hil_testing.py +977 -0
  419. oscura/{quality → validation/quality}/__init__.py +4 -4
  420. oscura/{quality → validation/quality}/ensemble.py +251 -171
  421. oscura/{quality → validation/quality}/explainer.py +3 -3
  422. oscura/{quality → validation/quality}/scoring.py +1 -1
  423. oscura/{quality → validation/quality}/warnings.py +4 -4
  424. oscura/validation/regression_suite.py +808 -0
  425. oscura/validation/replay.py +788 -0
  426. oscura/{testing → validation/testing}/__init__.py +2 -2
  427. oscura/{testing → validation/testing}/synthetic.py +5 -5
  428. oscura/visualization/__init__.py +9 -0
  429. oscura/visualization/accessibility.py +1 -1
  430. oscura/visualization/annotations.py +64 -67
  431. oscura/visualization/colors.py +7 -7
  432. oscura/visualization/digital.py +180 -81
  433. oscura/visualization/eye.py +236 -85
  434. oscura/visualization/interactive.py +320 -143
  435. oscura/visualization/jitter.py +587 -247
  436. oscura/visualization/layout.py +169 -134
  437. oscura/visualization/optimization.py +103 -52
  438. oscura/visualization/palettes.py +1 -1
  439. oscura/visualization/power.py +427 -211
  440. oscura/visualization/power_extended.py +626 -297
  441. oscura/visualization/presets.py +2 -0
  442. oscura/visualization/protocols.py +495 -181
  443. oscura/visualization/render.py +79 -63
  444. oscura/visualization/reverse_engineering.py +171 -124
  445. oscura/visualization/signal_integrity.py +460 -279
  446. oscura/visualization/specialized.py +190 -100
  447. oscura/visualization/spectral.py +670 -255
  448. oscura/visualization/thumbnails.py +166 -137
  449. oscura/visualization/waveform.py +150 -63
  450. oscura/workflows/__init__.py +3 -0
  451. oscura/{batch → workflows/batch}/__init__.py +5 -5
  452. oscura/{batch → workflows/batch}/advanced.py +150 -75
  453. oscura/workflows/batch/aggregate.py +531 -0
  454. oscura/workflows/batch/analyze.py +236 -0
  455. oscura/{batch → workflows/batch}/logging.py +2 -2
  456. oscura/{batch → workflows/batch}/metrics.py +1 -1
  457. oscura/workflows/complete_re.py +1144 -0
  458. oscura/workflows/compliance.py +44 -54
  459. oscura/workflows/digital.py +197 -51
  460. oscura/workflows/legacy/__init__.py +12 -0
  461. oscura/{workflow → workflows/legacy}/dag.py +4 -1
  462. oscura/workflows/multi_trace.py +9 -9
  463. oscura/workflows/power.py +42 -62
  464. oscura/workflows/protocol.py +82 -49
  465. oscura/workflows/reverse_engineering.py +351 -150
  466. oscura/workflows/signal_integrity.py +157 -82
  467. oscura-0.6.0.dist-info/METADATA +643 -0
  468. oscura-0.6.0.dist-info/RECORD +590 -0
  469. oscura/analyzers/digital/ic_database.py +0 -498
  470. oscura/analyzers/digital/timing_paths.py +0 -339
  471. oscura/analyzers/digital/vintage.py +0 -377
  472. oscura/analyzers/digital/vintage_result.py +0 -148
  473. oscura/analyzers/protocols/parallel_bus.py +0 -449
  474. oscura/batch/aggregate.py +0 -300
  475. oscura/batch/analyze.py +0 -139
  476. oscura/dsl/__init__.py +0 -73
  477. oscura/exceptions.py +0 -59
  478. oscura/exploratory/fuzzy.py +0 -513
  479. oscura/exploratory/sync.py +0 -384
  480. oscura/export/wavedrom.py +0 -430
  481. oscura/exporters/__init__.py +0 -94
  482. oscura/exporters/csv.py +0 -303
  483. oscura/exporters/exporters.py +0 -44
  484. oscura/exporters/hdf5.py +0 -217
  485. oscura/exporters/html_export.py +0 -701
  486. oscura/exporters/json_export.py +0 -338
  487. oscura/exporters/markdown_export.py +0 -367
  488. oscura/exporters/matlab_export.py +0 -354
  489. oscura/exporters/npz_export.py +0 -219
  490. oscura/exporters/spice_export.py +0 -210
  491. oscura/exporters/vintage_logic_csv.py +0 -247
  492. oscura/reporting/vintage_logic_report.py +0 -523
  493. oscura/search/context.py +0 -149
  494. oscura/session/__init__.py +0 -34
  495. oscura/session/annotations.py +0 -289
  496. oscura/session/history.py +0 -313
  497. oscura/session/session.py +0 -520
  498. oscura/visualization/digital_advanced.py +0 -718
  499. oscura/visualization/figure_manager.py +0 -156
  500. oscura/workflow/__init__.py +0 -13
  501. oscura-0.5.0.dist-info/METADATA +0 -407
  502. oscura-0.5.0.dist-info/RECORD +0 -486
  503. /oscura/core/{config.py → config/legacy.py} +0 -0
  504. /oscura/{extensibility → core/extensibility}/__init__.py +0 -0
  505. /oscura/{extensibility → core/extensibility}/registry.py +0 -0
  506. /oscura/{plugins → core/plugins}/isolation.py +0 -0
  507. /oscura/{builders → utils/builders}/signal_builder.py +0 -0
  508. /oscura/{optimization → utils/optimization}/parallel.py +0 -0
  509. /oscura/{pipeline → utils/pipeline}/__init__.py +0 -0
  510. /oscura/{streaming → utils/streaming}/__init__.py +0 -0
  511. {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/WHEEL +0 -0
  512. {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/entry_points.txt +0 -0
  513. {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,990 @@
1
+ """Advanced protocol fuzzer with coverage-guided mutation and structure-aware fuzzing.
2
+
3
+ This module provides comprehensive fuzzing capabilities for protocol testing, including
4
+ grammar-based mutation, coverage tracking, and crash detection. It integrates with the
5
+ existing grammar test framework and supports AFL-style corpus minimization.
6
+
7
+ Example:
8
+ >>> from oscura.validation import ProtocolFuzzer, FuzzingConfig
9
+ >>> from oscura.sessions import ProtocolSpec
10
+ >>>
11
+ >>> # Configure fuzzer
12
+ >>> config = FuzzingConfig(
13
+ ... strategy="coverage_guided",
14
+ ... max_iterations=1000,
15
+ ... crash_detection=True,
16
+ ... corpus_minimization=True
17
+ ... )
18
+ >>>
19
+ >>> # Run fuzzing campaign
20
+ >>> fuzzer = ProtocolFuzzer(config)
21
+ >>> result = fuzzer.fuzz_protocol(protocol_spec, seed_corpus)
22
+ >>>
23
+ >>> # Export results
24
+ >>> fuzzer.export_crashes(Path("crashes/"))
25
+ >>> fuzzer.export_corpus(Path("corpus/"))
26
+ >>> print(f"Found {len(result.crashes)} crashes")
27
+
28
+ References:
29
+ AFL Technical Details: https://lcamtuf.coredump.cx/afl/technical_details.txt
30
+ Coverage-Guided Fuzzing: Efficient Vulnerability Discovery by Michal Zalewski
31
+ Grammar-Based Fuzzing: Nautilus Paper (NDSS 2019)
32
+ """
33
+
34
+ from __future__ import annotations
35
+
36
+ import hashlib
37
+ import random
38
+ from collections.abc import Callable
39
+ from dataclasses import dataclass, field
40
+ from enum import Enum, auto
41
+ from pathlib import Path
42
+ from typing import TYPE_CHECKING, Any, ClassVar, Literal
43
+
44
+ if TYPE_CHECKING:
45
+ from oscura.sessions.blackbox import FieldHypothesis, ProtocolSpec
46
+
47
+
48
+ class FuzzingStrategy(Enum):
49
+ """Fuzzing strategy enumeration.
50
+
51
+ Attributes:
52
+ RANDOM: Pure random mutation without feedback.
53
+ MUTATION: AFL-style mutation-based fuzzing.
54
+ GENERATION: Grammar-based generation from protocol spec.
55
+ COVERAGE_GUIDED: Coverage feedback-guided fuzzing (most effective).
56
+ STRUCTURAL: Structure-aware field-level fuzzing.
57
+ """
58
+
59
+ RANDOM = auto()
60
+ MUTATION = auto()
61
+ GENERATION = auto()
62
+ COVERAGE_GUIDED = auto()
63
+ STRUCTURAL = auto()
64
+
65
+
66
+ class MutationOperator(Enum):
67
+ """Mutation operator types.
68
+
69
+ Attributes:
70
+ BIT_FLIP: Single bit flip.
71
+ BYTE_FLIP: Entire byte flip (XOR 0xFF).
72
+ ARITHMETIC: Arithmetic mutation (+1, -1, *2, /2).
73
+ BOUNDARY: Boundary value insertion (0, max, max+1).
74
+ SPECIAL: Special value insertion (0xFF, 0x00, 0x7F, 0x80).
75
+ INSERT: Byte insertion.
76
+ DELETE: Byte deletion.
77
+ DUPLICATE: Duplicate region.
78
+ SWAP: Swap two bytes.
79
+ CHECKSUM_CORRUPT: Intentionally corrupt checksum.
80
+ LENGTH_CORRUPT: Manipulate length fields.
81
+ """
82
+
83
+ BIT_FLIP = auto()
84
+ BYTE_FLIP = auto()
85
+ ARITHMETIC = auto()
86
+ BOUNDARY = auto()
87
+ SPECIAL = auto()
88
+ INSERT = auto()
89
+ DELETE = auto()
90
+ DUPLICATE = auto()
91
+ SWAP = auto()
92
+ CHECKSUM_CORRUPT = auto()
93
+ LENGTH_CORRUPT = auto()
94
+
95
+
96
+ class TestResult(Enum):
97
+ """Test execution result.
98
+
99
+ Attributes:
100
+ PASS: Test passed without errors.
101
+ FAIL: Test failed validation.
102
+ CRASH: Parser crashed or raised exception.
103
+ HANG: Test timed out (if timeout detection enabled).
104
+ UNKNOWN: Unable to determine result.
105
+ """
106
+
107
+ PASS = auto()
108
+ FAIL = auto()
109
+ CRASH = auto()
110
+ HANG = auto()
111
+ UNKNOWN = auto()
112
+
113
+
114
+ @dataclass
115
+ class FuzzingConfig:
116
+ """Configuration for protocol fuzzing.
117
+
118
+ Attributes:
119
+ strategy: Fuzzing strategy to use.
120
+ max_iterations: Maximum number of fuzzing iterations.
121
+ timeout_ms: Timeout in milliseconds per test case.
122
+ crash_detection: Enable crash detection.
123
+ hang_detection: Enable hang/timeout detection.
124
+ corpus_minimization: Enable AFL-style corpus minimization.
125
+ coverage_tracking: Track code coverage (requires instrumentation).
126
+ mutation_operators: List of enabled mutation operators (None = all).
127
+ seed: Random seed for reproducibility (None = random).
128
+ export_crashes: Export crash-inducing inputs.
129
+ export_pcap: Export fuzzed packets as PCAP.
130
+ min_corpus_size: Minimum corpus size to maintain.
131
+ max_corpus_size: Maximum corpus size (for minimization).
132
+
133
+ Example:
134
+ >>> config = FuzzingConfig(
135
+ ... strategy="coverage_guided",
136
+ ... max_iterations=10000,
137
+ ... crash_detection=True,
138
+ ... corpus_minimization=True,
139
+ ... seed=42
140
+ ... )
141
+ """
142
+
143
+ strategy: Literal["random", "mutation", "generation", "coverage_guided", "structural"] = (
144
+ "coverage_guided"
145
+ )
146
+ max_iterations: int = 1000
147
+ timeout_ms: int = 1000
148
+ crash_detection: bool = True
149
+ hang_detection: bool = True
150
+ corpus_minimization: bool = True
151
+ coverage_tracking: bool = True
152
+ mutation_operators: list[str] | None = None
153
+ seed: int | None = None
154
+ export_crashes: bool = True
155
+ export_pcap: bool = False
156
+ min_corpus_size: int = 10
157
+ max_corpus_size: int = 1000
158
+
159
+ def __post_init__(self) -> None:
160
+ """Validate configuration after initialization."""
161
+ if self.max_iterations <= 0:
162
+ raise ValueError(f"max_iterations must be positive, got {self.max_iterations}")
163
+ if self.timeout_ms <= 0:
164
+ raise ValueError(f"timeout_ms must be positive, got {self.timeout_ms}")
165
+ if self.strategy not in {
166
+ "random",
167
+ "mutation",
168
+ "generation",
169
+ "coverage_guided",
170
+ "structural",
171
+ }:
172
+ raise ValueError(f"Invalid strategy: {self.strategy}")
173
+ if self.min_corpus_size < 0:
174
+ raise ValueError(f"min_corpus_size must be non-negative, got {self.min_corpus_size}")
175
+ if self.max_corpus_size < self.min_corpus_size:
176
+ raise ValueError("max_corpus_size must be >= min_corpus_size")
177
+
178
+
179
+ @dataclass
180
+ class FuzzingResult:
181
+ """Result of a single fuzzing test case.
182
+
183
+ Attributes:
184
+ test_case: Input that was tested.
185
+ result: Test execution result (pass/fail/crash/hang).
186
+ coverage_delta: New coverage branches discovered (if tracking enabled).
187
+ mutation_applied: Mutation operator that was applied.
188
+ execution_time_ms: Execution time in milliseconds.
189
+ error_message: Error message if crash occurred.
190
+ stack_trace: Stack trace if crash occurred.
191
+
192
+ Example:
193
+ >>> result = FuzzingResult(
194
+ ... test_case=b"\\xaa\\xff\\x00\\x12",
195
+ ... result=TestResult.CRASH,
196
+ ... mutation_applied=MutationOperator.CHECKSUM_CORRUPT,
197
+ ... error_message="IndexError: list index out of range"
198
+ ... )
199
+ """
200
+
201
+ test_case: bytes
202
+ result: TestResult
203
+ coverage_delta: int = 0
204
+ mutation_applied: MutationOperator | None = None
205
+ execution_time_ms: float = 0.0
206
+ error_message: str = ""
207
+ stack_trace: str = ""
208
+
209
+
210
+ @dataclass
211
+ class FuzzingReport:
212
+ """Comprehensive fuzzing campaign report.
213
+
214
+ Attributes:
215
+ total_iterations: Total fuzzing iterations executed.
216
+ total_crashes: Number of unique crashes found.
217
+ total_hangs: Number of timeouts/hangs.
218
+ total_coverage_branches: Total code coverage branches discovered.
219
+ corpus_size: Final corpus size after minimization.
220
+ crashes: List of crash-inducing inputs.
221
+ interesting_inputs: Inputs that increased coverage.
222
+ mutation_stats: Statistics per mutation operator.
223
+ coverage_history: Coverage over time (for graphing).
224
+ execution_time_seconds: Total fuzzing campaign time.
225
+
226
+ Example:
227
+ >>> report = FuzzingReport(
228
+ ... total_iterations=10000,
229
+ ... total_crashes=5,
230
+ ... corpus_size=127,
231
+ ... coverage_history=[10, 25, 43, 68, 89]
232
+ ... )
233
+ >>> print(f"Crash rate: {report.total_crashes / report.total_iterations:.2%}")
234
+ """
235
+
236
+ total_iterations: int = 0
237
+ total_crashes: int = 0
238
+ total_hangs: int = 0
239
+ total_coverage_branches: int = 0
240
+ corpus_size: int = 0
241
+ crashes: list[bytes] = field(default_factory=list)
242
+ interesting_inputs: list[bytes] = field(default_factory=list)
243
+ mutation_stats: dict[str, int] = field(default_factory=dict)
244
+ coverage_history: list[int] = field(default_factory=list)
245
+ execution_time_seconds: float = 0.0
246
+
247
+ @property
248
+ def crash_rate(self) -> float:
249
+ """Calculate crash rate.
250
+
251
+ Returns:
252
+ Proportion of inputs that crashed (0.0 to 1.0).
253
+ """
254
+ if self.total_iterations == 0:
255
+ return 0.0
256
+ return self.total_crashes / self.total_iterations
257
+
258
+ @property
259
+ def unique_crashes(self) -> int:
260
+ """Get count of unique crashes.
261
+
262
+ Returns:
263
+ Number of unique crash-inducing inputs.
264
+ """
265
+ return len(set(self.crashes))
266
+
267
+
268
+ class ProtocolFuzzer:
269
+ """Advanced protocol fuzzer with coverage-guided mutation.
270
+
271
+ Implements AFL-inspired fuzzing with structure-aware mutations for protocol
272
+ reverse engineering. Tracks code coverage, detects crashes, and maintains
273
+ a minimized corpus of interesting test cases.
274
+
275
+ Attributes:
276
+ config: Fuzzing configuration.
277
+
278
+ Example:
279
+ >>> fuzzer = ProtocolFuzzer(FuzzingConfig(max_iterations=5000))
280
+ >>> report = fuzzer.fuzz_protocol(protocol_spec, seed_corpus)
281
+ >>> print(f"Found {report.total_crashes} crashes")
282
+ >>> fuzzer.export_crashes(Path("crashes/"))
283
+ """
284
+
285
+ # Special byte values for boundary fuzzing
286
+ SPECIAL_BYTES: ClassVar[list[int]] = [0x00, 0x01, 0x7F, 0x80, 0xFF, 0x10, 0x20, 0x40]
287
+
288
+ def __init__(self, config: FuzzingConfig) -> None:
289
+ """Initialize protocol fuzzer.
290
+
291
+ Args:
292
+ config: Fuzzing configuration.
293
+ """
294
+ self.config = config
295
+ self._rng = random.Random(config.seed if config.seed is not None else None)
296
+ self._corpus: list[bytes] = []
297
+ self._coverage_map: set[int] = set()
298
+ self._crash_hashes: set[str] = set()
299
+ self._report = FuzzingReport()
300
+
301
+ def fuzz_protocol(
302
+ self,
303
+ spec: ProtocolSpec,
304
+ seed_corpus: list[bytes] | None = None,
305
+ target_function: Callable[[bytes], Any] | None = None,
306
+ ) -> FuzzingReport:
307
+ """Execute fuzzing campaign on protocol.
308
+
309
+ Args:
310
+ spec: Protocol specification for structure-aware fuzzing.
311
+ seed_corpus: Initial corpus of valid messages (None = generate from spec).
312
+ target_function: Target parser function to test (None = dry run).
313
+
314
+ Returns:
315
+ Comprehensive fuzzing report with crashes and coverage.
316
+
317
+ Example:
318
+ >>> def parse_message(data: bytes) -> dict:
319
+ ... # Parser implementation
320
+ ... return {"parsed": True}
321
+ >>>
322
+ >>> report = fuzzer.fuzz_protocol(spec, seed_corpus, parse_message)
323
+ >>> print(f"Coverage: {report.total_coverage_branches} branches")
324
+ """
325
+ import time
326
+
327
+ start_time = time.time()
328
+
329
+ # Initialize corpus
330
+ self._initialize_corpus(spec, seed_corpus)
331
+
332
+ # Run fuzzing iterations
333
+ for iteration in range(self.config.max_iterations):
334
+ # Select input from corpus
335
+ base_input = self._select_input()
336
+
337
+ # Mutate input based on strategy
338
+ mutated_input, mutation_op = self._mutate_input(base_input, spec)
339
+
340
+ # Execute target function
341
+ result = self._execute_target(mutated_input, target_function)
342
+
343
+ # Update corpus and coverage
344
+ self._update_corpus(mutated_input, result)
345
+
346
+ # Record statistics
347
+ self._update_statistics(result, mutation_op)
348
+
349
+ # Track coverage history periodically
350
+ if iteration % 100 == 0:
351
+ self._report.coverage_history.append(self._report.total_coverage_branches)
352
+
353
+ # Minimize corpus if enabled
354
+ if self.config.corpus_minimization:
355
+ self._minimize_corpus()
356
+
357
+ self._report.execution_time_seconds = time.time() - start_time
358
+ self._report.corpus_size = len(self._corpus)
359
+
360
+ return self._report
361
+
362
+ def _initialize_corpus(self, spec: ProtocolSpec, seed_corpus: list[bytes] | None) -> None:
363
+ """Initialize fuzzing corpus.
364
+
365
+ Args:
366
+ spec: Protocol specification.
367
+ seed_corpus: Optional seed corpus (None = generate from spec).
368
+ """
369
+ if seed_corpus:
370
+ self._corpus = list(seed_corpus)
371
+ else:
372
+ # Generate seed corpus from protocol spec
373
+ self._corpus = self._generate_seed_corpus(spec)
374
+
375
+ def _generate_seed_corpus(self, spec: ProtocolSpec) -> list[bytes]:
376
+ """Generate initial seed corpus from protocol specification.
377
+
378
+ Args:
379
+ spec: Protocol specification.
380
+
381
+ Returns:
382
+ List of valid seed messages.
383
+
384
+ Example:
385
+ >>> corpus = fuzzer._generate_seed_corpus(spec)
386
+ >>> all(isinstance(msg, bytes) for msg in corpus)
387
+ True
388
+ """
389
+ corpus: list[bytes] = []
390
+ num_seeds = max(self.config.min_corpus_size, 20)
391
+
392
+ for _ in range(num_seeds):
393
+ msg = bytearray()
394
+
395
+ for field_def in spec.fields:
396
+ field_bytes = self._generate_field_value(field_def)
397
+ msg.extend(field_bytes)
398
+
399
+ corpus.append(bytes(msg))
400
+
401
+ return corpus
402
+
403
+ def _generate_field_value(self, field_def: FieldHypothesis) -> bytes:
404
+ """Generate value for a single field.
405
+
406
+ Args:
407
+ field_def: Field definition.
408
+
409
+ Returns:
410
+ Field value as bytes.
411
+ """
412
+ if field_def.field_type == "constant":
413
+ const_val = field_def.evidence.get("value", 0)
414
+ return self._pack_value(const_val, field_def.length)
415
+
416
+ if field_def.field_type == "counter":
417
+ counter_val = self._rng.randint(0, (256**field_def.length) - 1)
418
+ return self._pack_value(counter_val, field_def.length)
419
+
420
+ if field_def.field_type == "checksum":
421
+ return b"\x00" * field_def.length
422
+
423
+ # Default: random data
424
+ return bytes(self._rng.randint(0, 255) for _ in range(field_def.length))
425
+
426
+ def _select_input(self) -> bytes:
427
+ """Select input from corpus for mutation.
428
+
429
+ Returns:
430
+ Selected input bytes.
431
+ """
432
+ if not self._corpus:
433
+ return b""
434
+
435
+ # For coverage-guided fuzzing, favor inputs with higher coverage
436
+ if self.config.strategy == "coverage_guided" and len(self._corpus) > 5:
437
+ # Simple heuristic: favor recent additions (likely higher coverage)
438
+ return self._rng.choice(self._corpus[-min(20, len(self._corpus)) :])
439
+
440
+ return self._rng.choice(self._corpus)
441
+
442
+ def _mutate_input(
443
+ self, input_data: bytes, spec: ProtocolSpec
444
+ ) -> tuple[bytes, MutationOperator]:
445
+ """Mutate input based on fuzzing strategy.
446
+
447
+ Args:
448
+ input_data: Original input.
449
+ spec: Protocol specification for structure-aware mutations.
450
+
451
+ Returns:
452
+ Tuple of (mutated_input, mutation_operator_applied).
453
+
454
+ Example:
455
+ >>> mutated, op = fuzzer._mutate_input(b"\\xaa\\x01\\x00", spec)
456
+ >>> isinstance(mutated, bytes)
457
+ True
458
+ """
459
+ if not input_data:
460
+ return b"\x00", MutationOperator.INSERT
461
+
462
+ # Select mutation operator
463
+ mutation_op = self._select_mutation_operator()
464
+
465
+ # Apply mutation
466
+ mutated = self._apply_mutation(input_data, mutation_op, spec)
467
+
468
+ return mutated, mutation_op
469
+
470
+ def _select_mutation_operator(self) -> MutationOperator:
471
+ """Select mutation operator based on configuration.
472
+
473
+ Returns:
474
+ Selected mutation operator.
475
+ """
476
+ # Filter operators based on config
477
+ if self.config.mutation_operators:
478
+ available = [op for op in MutationOperator if op.name in self.config.mutation_operators]
479
+ else:
480
+ available = list(MutationOperator)
481
+
482
+ return self._rng.choice(available)
483
+
484
+ def _apply_mutation(self, data: bytes, operator: MutationOperator, spec: ProtocolSpec) -> bytes:
485
+ """Apply mutation operator to data.
486
+
487
+ Args:
488
+ data: Original data.
489
+ operator: Mutation operator to apply.
490
+ spec: Protocol specification.
491
+
492
+ Returns:
493
+ Mutated data.
494
+
495
+ Example:
496
+ >>> mutated = fuzzer._apply_mutation(b"\\xaa\\x01", MutationOperator.BIT_FLIP, spec)
497
+ >>> len(mutated) > 0
498
+ True
499
+ """
500
+ msg = bytearray(data)
501
+
502
+ if not msg:
503
+ return bytes(msg)
504
+
505
+ # Map mutation operators to handler functions
506
+ mutation_handlers: dict[MutationOperator, Any] = {
507
+ MutationOperator.BIT_FLIP: self._mutate_bit_flip,
508
+ MutationOperator.BYTE_FLIP: self._mutate_byte_flip,
509
+ MutationOperator.ARITHMETIC: self._mutate_arithmetic,
510
+ MutationOperator.BOUNDARY: self._mutate_boundary,
511
+ MutationOperator.SPECIAL: self._mutate_special,
512
+ MutationOperator.INSERT: self._mutate_insert,
513
+ MutationOperator.DELETE: self._mutate_delete,
514
+ MutationOperator.DUPLICATE: self._mutate_duplicate,
515
+ MutationOperator.SWAP: self._mutate_swap,
516
+ MutationOperator.CHECKSUM_CORRUPT: lambda m: bytearray(
517
+ self._corrupt_checksum(bytes(m), spec)
518
+ ),
519
+ MutationOperator.LENGTH_CORRUPT: lambda m: bytearray(
520
+ self._corrupt_length_field(bytes(m), spec)
521
+ ),
522
+ }
523
+
524
+ handler = mutation_handlers.get(operator)
525
+ if handler:
526
+ msg = handler(msg)
527
+
528
+ return bytes(msg)
529
+
530
+ def _mutate_bit_flip(self, msg: bytearray) -> bytearray:
531
+ """Apply bit flip mutation.
532
+
533
+ Args:
534
+ msg: Message to mutate.
535
+
536
+ Returns:
537
+ Mutated message.
538
+ """
539
+ pos = self._rng.randint(0, len(msg) - 1)
540
+ bit = self._rng.randint(0, 7)
541
+ msg[pos] ^= 1 << bit
542
+ return msg
543
+
544
+ def _mutate_byte_flip(self, msg: bytearray) -> bytearray:
545
+ """Apply byte flip mutation.
546
+
547
+ Args:
548
+ msg: Message to mutate.
549
+
550
+ Returns:
551
+ Mutated message.
552
+ """
553
+ pos = self._rng.randint(0, len(msg) - 1)
554
+ msg[pos] ^= 0xFF
555
+ return msg
556
+
557
+ def _mutate_arithmetic(self, msg: bytearray) -> bytearray:
558
+ """Apply arithmetic mutation.
559
+
560
+ Args:
561
+ msg: Message to mutate.
562
+
563
+ Returns:
564
+ Mutated message.
565
+ """
566
+ pos = self._rng.randint(0, len(msg) - 1)
567
+ delta = self._rng.choice([-1, 1, -16, 16, -256, 256])
568
+ msg[pos] = (msg[pos] + delta) % 256
569
+ return msg
570
+
571
+ def _mutate_boundary(self, msg: bytearray) -> bytearray:
572
+ """Apply boundary value mutation.
573
+
574
+ Args:
575
+ msg: Message to mutate.
576
+
577
+ Returns:
578
+ Mutated message.
579
+ """
580
+ pos = self._rng.randint(0, len(msg) - 1)
581
+ msg[pos] = self._rng.choice([0x00, 0xFF, 0x7F, 0x80])
582
+ return msg
583
+
584
+ def _mutate_special(self, msg: bytearray) -> bytearray:
585
+ """Apply special value mutation.
586
+
587
+ Args:
588
+ msg: Message to mutate.
589
+
590
+ Returns:
591
+ Mutated message.
592
+ """
593
+ pos = self._rng.randint(0, len(msg) - 1)
594
+ msg[pos] = self._rng.choice(self.SPECIAL_BYTES)
595
+ return msg
596
+
597
+ def _mutate_insert(self, msg: bytearray) -> bytearray:
598
+ """Apply byte insertion mutation.
599
+
600
+ Args:
601
+ msg: Message to mutate.
602
+
603
+ Returns:
604
+ Mutated message.
605
+ """
606
+ pos = self._rng.randint(0, len(msg))
607
+ msg.insert(pos, self._rng.randint(0, 255))
608
+ return msg
609
+
610
+ def _mutate_delete(self, msg: bytearray) -> bytearray:
611
+ """Apply byte deletion mutation.
612
+
613
+ Args:
614
+ msg: Message to mutate.
615
+
616
+ Returns:
617
+ Mutated message.
618
+ """
619
+ if len(msg) > 1:
620
+ pos = self._rng.randint(0, len(msg) - 1)
621
+ del msg[pos]
622
+ return msg
623
+
624
+ def _mutate_duplicate(self, msg: bytearray) -> bytearray:
625
+ """Apply region duplication mutation.
626
+
627
+ Args:
628
+ msg: Message to mutate.
629
+
630
+ Returns:
631
+ Mutated message.
632
+ """
633
+ if len(msg) >= 2:
634
+ start = self._rng.randint(0, len(msg) - 2)
635
+ length = self._rng.randint(1, min(8, len(msg) - start))
636
+ region = msg[start : start + length]
637
+ pos = self._rng.randint(0, len(msg))
638
+ msg[pos:pos] = region
639
+ return msg
640
+
641
+ def _mutate_swap(self, msg: bytearray) -> bytearray:
642
+ """Apply byte swap mutation.
643
+
644
+ Args:
645
+ msg: Message to mutate.
646
+
647
+ Returns:
648
+ Mutated message.
649
+ """
650
+ if len(msg) >= 2:
651
+ pos1 = self._rng.randint(0, len(msg) - 1)
652
+ pos2 = self._rng.randint(0, len(msg) - 1)
653
+ msg[pos1], msg[pos2] = msg[pos2], msg[pos1]
654
+ return msg
655
+
656
+ def _corrupt_checksum(self, data: bytes, spec: ProtocolSpec) -> bytes:
657
+ """Corrupt checksum field in message.
658
+
659
+ Args:
660
+ data: Original message.
661
+ spec: Protocol specification.
662
+
663
+ Returns:
664
+ Message with corrupted checksum.
665
+ """
666
+ # Find checksum field
667
+ checksum_offset = 0
668
+ checksum_length = 0
669
+ for field_def in spec.fields:
670
+ if field_def.field_type == "checksum":
671
+ checksum_length = field_def.length
672
+ break
673
+ checksum_offset += field_def.length
674
+
675
+ if checksum_length == 0:
676
+ return data # No checksum field
677
+
678
+ msg = bytearray(data)
679
+ if checksum_offset + checksum_length <= len(msg):
680
+ for i in range(checksum_length):
681
+ msg[checksum_offset + i] ^= self._rng.randint(1, 255)
682
+
683
+ return bytes(msg)
684
+
685
+ def _corrupt_length_field(self, data: bytes, spec: ProtocolSpec) -> bytes:
686
+ """Manipulate length field (overflow/underflow).
687
+
688
+ Args:
689
+ data: Original message.
690
+ spec: Protocol specification.
691
+
692
+ Returns:
693
+ Message with corrupted length field.
694
+ """
695
+ # Find length-like fields (heuristic: fields named "length" or "len")
696
+ length_offset = 0
697
+ length_length = 0
698
+ for field_def in spec.fields:
699
+ field_name = field_def.name.lower()
700
+ if "length" in field_name or field_name == "len":
701
+ length_length = field_def.length
702
+ break
703
+ length_offset += field_def.length
704
+
705
+ if length_length == 0:
706
+ return data # No length field
707
+
708
+ msg = bytearray(data)
709
+ if length_offset + length_length <= len(msg):
710
+ # Extract current length
711
+ length_bytes = msg[length_offset : length_offset + length_length]
712
+ current_len = int.from_bytes(length_bytes, byteorder="little")
713
+
714
+ # Corrupt with overflow/underflow
715
+ corruption = self._rng.choice(
716
+ [
717
+ current_len + 1, # Overflow
718
+ max(0, current_len - 1), # Underflow
719
+ 0, # Zero length
720
+ (256**length_length) - 1, # Max value
721
+ ]
722
+ )
723
+
724
+ # Pack back
725
+ msg[length_offset : length_offset + length_length] = corruption.to_bytes(
726
+ length_length, byteorder="little"
727
+ )
728
+
729
+ return bytes(msg)
730
+
731
+ def _execute_target(
732
+ self, test_case: bytes, target_function: Callable[[bytes], Any] | None
733
+ ) -> FuzzingResult:
734
+ """Execute target function with test case.
735
+
736
+ Args:
737
+ test_case: Input to test.
738
+ target_function: Parser function to execute (None = dry run).
739
+
740
+ Returns:
741
+ Fuzzing result with execution outcome.
742
+
743
+ Example:
744
+ >>> def parser(data: bytes) -> dict:
745
+ ... return {"valid": True}
746
+ >>> result = fuzzer._execute_target(b"\\xaa\\x01", parser)
747
+ >>> result.result in [TestResult.PASS, TestResult.FAIL, TestResult.CRASH]
748
+ True
749
+ """
750
+ import time
751
+ import traceback
752
+
753
+ result = FuzzingResult(test_case=test_case, result=TestResult.UNKNOWN)
754
+
755
+ if target_function is None:
756
+ # Dry run - assume pass
757
+ result.result = TestResult.PASS
758
+ return result
759
+
760
+ start_time = time.time()
761
+
762
+ try:
763
+ # Execute target with timeout (if hang detection enabled)
764
+ target_function(test_case)
765
+ result.result = TestResult.PASS
766
+
767
+ # Simulate coverage tracking (in real implementation, would use instrumentation)
768
+ coverage_hash = self._compute_coverage_hash(test_case)
769
+ if coverage_hash not in self._coverage_map:
770
+ self._coverage_map.add(coverage_hash)
771
+ result.coverage_delta = 1
772
+
773
+ except Exception as e:
774
+ # Crash detected
775
+ result.result = TestResult.CRASH
776
+ result.error_message = str(e)
777
+ result.stack_trace = traceback.format_exc()
778
+
779
+ finally:
780
+ result.execution_time_ms = (time.time() - start_time) * 1000
781
+
782
+ return result
783
+
784
+ def _compute_coverage_hash(self, data: bytes) -> int:
785
+ """Compute coverage hash for input (simulates code coverage).
786
+
787
+ In a real implementation, this would use instrumentation (e.g., AFL's edge coverage).
788
+ For now, we use a simple hash of the input as a proxy.
789
+
790
+ Args:
791
+ data: Input data.
792
+
793
+ Returns:
794
+ Coverage hash.
795
+ """
796
+ # Simple hash based on input characteristics
797
+ return hash((len(data), data[: min(4, len(data))], data[-min(4, len(data)) :]))
798
+
799
+ def _update_corpus(self, test_case: bytes, result: FuzzingResult) -> None:
800
+ """Update corpus based on fuzzing result.
801
+
802
+ Args:
803
+ test_case: Test case that was executed.
804
+ result: Execution result.
805
+ """
806
+ if result.result == TestResult.CRASH:
807
+ # Add to crashes (deduplicate by hash)
808
+ crash_hash = hashlib.sha256(test_case).hexdigest()
809
+ if crash_hash not in self._crash_hashes:
810
+ self._crash_hashes.add(crash_hash)
811
+ self._report.crashes.append(test_case)
812
+
813
+ if result.coverage_delta > 0:
814
+ # Add to interesting inputs
815
+ self._report.interesting_inputs.append(test_case)
816
+
817
+ # Add to corpus if not too large
818
+ if len(self._corpus) < self.config.max_corpus_size:
819
+ self._corpus.append(test_case)
820
+
821
+ def _update_statistics(self, result: FuzzingResult, mutation_op: MutationOperator) -> None:
822
+ """Update fuzzing statistics.
823
+
824
+ Args:
825
+ result: Fuzzing result.
826
+ mutation_op: Mutation operator that was applied.
827
+ """
828
+ self._report.total_iterations += 1
829
+
830
+ if result.result == TestResult.CRASH:
831
+ self._report.total_crashes += 1
832
+
833
+ if result.result == TestResult.HANG:
834
+ self._report.total_hangs += 1
835
+
836
+ if result.coverage_delta > 0:
837
+ self._report.total_coverage_branches += result.coverage_delta
838
+
839
+ # Track mutation operator stats
840
+ op_name = mutation_op.name
841
+ self._report.mutation_stats[op_name] = self._report.mutation_stats.get(op_name, 0) + 1
842
+
843
+ def _minimize_corpus(self) -> None:
844
+ """Minimize corpus using AFL-style algorithm.
845
+
846
+ Keeps only inputs that contribute unique coverage, removing redundant inputs.
847
+ """
848
+ if len(self._corpus) <= self.config.min_corpus_size:
849
+ return
850
+
851
+ # Track which coverage branches each input covers
852
+ coverage_per_input: dict[int, set[int]] = {}
853
+ for idx, test_case in enumerate(self._corpus):
854
+ coverage_per_input[idx] = {self._compute_coverage_hash(test_case)}
855
+
856
+ # Greedy set cover: keep inputs that cover unique branches
857
+ minimized: list[bytes] = []
858
+ covered_branches: set[int] = set()
859
+
860
+ while coverage_per_input:
861
+ # Find input that covers most uncovered branches
862
+ best_idx = max(
863
+ coverage_per_input.keys(),
864
+ key=lambda i: len(coverage_per_input[i] - covered_branches),
865
+ )
866
+
867
+ # Add to minimized corpus
868
+ minimized.append(self._corpus[best_idx])
869
+ covered_branches.update(coverage_per_input[best_idx])
870
+
871
+ # Remove this input
872
+ del coverage_per_input[best_idx]
873
+
874
+ # Stop if we've covered everything or reached max size
875
+ if len(minimized) >= self.config.max_corpus_size:
876
+ break
877
+
878
+ self._corpus = minimized
879
+
880
+ def _pack_value(self, value: int, length: int) -> bytes:
881
+ """Pack integer value into bytes (little-endian).
882
+
883
+ Args:
884
+ value: Integer value.
885
+ length: Number of bytes.
886
+
887
+ Returns:
888
+ Packed bytes.
889
+
890
+ Example:
891
+ >>> fuzzer._pack_value(0x1234, 2)
892
+ b'\\x34\\x12'
893
+ """
894
+ return value.to_bytes(length, byteorder="little")
895
+
896
+ def export_crashes(self, output_dir: Path) -> None:
897
+ """Export crash-inducing inputs to directory.
898
+
899
+ Args:
900
+ output_dir: Output directory for crash files.
901
+
902
+ Example:
903
+ >>> fuzzer.export_crashes(Path("crashes/"))
904
+ """
905
+ output_dir.mkdir(parents=True, exist_ok=True)
906
+
907
+ for idx, crash in enumerate(self._report.crashes):
908
+ crash_file = output_dir / f"crash_{idx:04d}.bin"
909
+ crash_file.write_bytes(crash)
910
+
911
+ def export_corpus(self, output_dir: Path) -> None:
912
+ """Export minimized corpus to directory.
913
+
914
+ Args:
915
+ output_dir: Output directory for corpus files.
916
+
917
+ Example:
918
+ >>> fuzzer.export_corpus(Path("corpus/"))
919
+ """
920
+ output_dir.mkdir(parents=True, exist_ok=True)
921
+
922
+ for idx, test_case in enumerate(self._corpus):
923
+ corpus_file = output_dir / f"input_{idx:04d}.bin"
924
+ corpus_file.write_bytes(test_case)
925
+
926
+ def export_pcap(self, messages: list[bytes], output: Path) -> None:
927
+ """Export fuzzed messages as PCAP file.
928
+
929
+ Args:
930
+ messages: Protocol messages to export.
931
+ output: Output PCAP file path.
932
+
933
+ Example:
934
+ >>> fuzzer.export_pcap(fuzzer._corpus, Path("corpus.pcap"))
935
+ """
936
+ try:
937
+ from scapy.all import ( # type: ignore[attr-defined]
938
+ IP,
939
+ UDP,
940
+ Ether,
941
+ wrpcap,
942
+ )
943
+ except ImportError as e:
944
+ raise ImportError(
945
+ "scapy is required for PCAP export. Install with: uv pip install scapy"
946
+ ) from e
947
+
948
+ packets = []
949
+ for msg in messages:
950
+ pkt = Ether() / IP() / UDP(sport=12345, dport=54321) / msg
951
+ packets.append(pkt)
952
+
953
+ wrpcap(str(output), packets)
954
+
955
+ def export_report(self, output: Path) -> None:
956
+ """Export fuzzing report as JSON.
957
+
958
+ Args:
959
+ output: Output JSON file path.
960
+
961
+ Example:
962
+ >>> fuzzer.export_report(Path("fuzzing_report.json"))
963
+ """
964
+ import json
965
+
966
+ report_data = {
967
+ "total_iterations": self._report.total_iterations,
968
+ "total_crashes": self._report.total_crashes,
969
+ "unique_crashes": self._report.unique_crashes,
970
+ "total_hangs": self._report.total_hangs,
971
+ "total_coverage_branches": self._report.total_coverage_branches,
972
+ "corpus_size": self._report.corpus_size,
973
+ "crash_rate": self._report.crash_rate,
974
+ "mutation_stats": self._report.mutation_stats,
975
+ "coverage_history": self._report.coverage_history,
976
+ "execution_time_seconds": self._report.execution_time_seconds,
977
+ }
978
+
979
+ output.write_text(json.dumps(report_data, indent=2))
980
+
981
+
982
+ __all__ = [
983
+ "FuzzingConfig",
984
+ "FuzzingReport",
985
+ "FuzzingResult",
986
+ "FuzzingStrategy",
987
+ "MutationOperator",
988
+ "ProtocolFuzzer",
989
+ "TestResult",
990
+ ]