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.
- oscura/__init__.py +169 -167
- oscura/analyzers/__init__.py +3 -0
- oscura/analyzers/classification.py +659 -0
- oscura/analyzers/digital/__init__.py +0 -48
- oscura/analyzers/digital/edges.py +325 -65
- oscura/analyzers/digital/extraction.py +0 -195
- oscura/analyzers/digital/quality.py +293 -166
- oscura/analyzers/digital/timing.py +260 -115
- oscura/analyzers/digital/timing_numba.py +334 -0
- oscura/analyzers/entropy.py +605 -0
- oscura/analyzers/eye/diagram.py +176 -109
- oscura/analyzers/eye/metrics.py +5 -5
- oscura/analyzers/jitter/__init__.py +6 -4
- oscura/analyzers/jitter/ber.py +52 -52
- oscura/analyzers/jitter/classification.py +156 -0
- oscura/analyzers/jitter/decomposition.py +163 -113
- oscura/analyzers/jitter/spectrum.py +80 -64
- oscura/analyzers/ml/__init__.py +39 -0
- oscura/analyzers/ml/features.py +600 -0
- oscura/analyzers/ml/signal_classifier.py +604 -0
- oscura/analyzers/packet/daq.py +246 -158
- oscura/analyzers/packet/parser.py +12 -1
- oscura/analyzers/packet/payload.py +50 -2110
- oscura/analyzers/packet/payload_analysis.py +361 -181
- oscura/analyzers/packet/payload_patterns.py +133 -70
- oscura/analyzers/packet/stream.py +84 -23
- oscura/analyzers/patterns/__init__.py +26 -5
- oscura/analyzers/patterns/anomaly_detection.py +908 -0
- oscura/analyzers/patterns/clustering.py +169 -108
- oscura/analyzers/patterns/clustering_optimized.py +227 -0
- oscura/analyzers/patterns/discovery.py +1 -1
- oscura/analyzers/patterns/matching.py +581 -197
- oscura/analyzers/patterns/pattern_mining.py +778 -0
- oscura/analyzers/patterns/periodic.py +121 -38
- oscura/analyzers/patterns/sequences.py +175 -78
- oscura/analyzers/power/conduction.py +1 -1
- oscura/analyzers/power/soa.py +6 -6
- oscura/analyzers/power/switching.py +250 -110
- oscura/analyzers/protocol/__init__.py +17 -1
- oscura/analyzers/protocols/__init__.py +1 -22
- oscura/analyzers/protocols/base.py +6 -6
- oscura/analyzers/protocols/ble/__init__.py +38 -0
- oscura/analyzers/protocols/ble/analyzer.py +809 -0
- oscura/analyzers/protocols/ble/uuids.py +288 -0
- oscura/analyzers/protocols/can.py +257 -127
- oscura/analyzers/protocols/can_fd.py +107 -80
- oscura/analyzers/protocols/flexray.py +139 -80
- oscura/analyzers/protocols/hdlc.py +93 -58
- oscura/analyzers/protocols/i2c.py +247 -106
- oscura/analyzers/protocols/i2s.py +138 -86
- oscura/analyzers/protocols/industrial/__init__.py +40 -0
- oscura/analyzers/protocols/industrial/bacnet/__init__.py +33 -0
- oscura/analyzers/protocols/industrial/bacnet/analyzer.py +708 -0
- oscura/analyzers/protocols/industrial/bacnet/encoding.py +412 -0
- oscura/analyzers/protocols/industrial/bacnet/services.py +622 -0
- oscura/analyzers/protocols/industrial/ethercat/__init__.py +30 -0
- oscura/analyzers/protocols/industrial/ethercat/analyzer.py +474 -0
- oscura/analyzers/protocols/industrial/ethercat/mailbox.py +339 -0
- oscura/analyzers/protocols/industrial/ethercat/topology.py +166 -0
- oscura/analyzers/protocols/industrial/modbus/__init__.py +31 -0
- oscura/analyzers/protocols/industrial/modbus/analyzer.py +525 -0
- oscura/analyzers/protocols/industrial/modbus/crc.py +79 -0
- oscura/analyzers/protocols/industrial/modbus/functions.py +436 -0
- oscura/analyzers/protocols/industrial/opcua/__init__.py +21 -0
- oscura/analyzers/protocols/industrial/opcua/analyzer.py +552 -0
- oscura/analyzers/protocols/industrial/opcua/datatypes.py +446 -0
- oscura/analyzers/protocols/industrial/opcua/services.py +264 -0
- oscura/analyzers/protocols/industrial/profinet/__init__.py +23 -0
- oscura/analyzers/protocols/industrial/profinet/analyzer.py +441 -0
- oscura/analyzers/protocols/industrial/profinet/dcp.py +263 -0
- oscura/analyzers/protocols/industrial/profinet/ptcp.py +200 -0
- oscura/analyzers/protocols/jtag.py +180 -98
- oscura/analyzers/protocols/lin.py +219 -114
- oscura/analyzers/protocols/manchester.py +4 -4
- oscura/analyzers/protocols/onewire.py +253 -149
- oscura/analyzers/protocols/parallel_bus/__init__.py +20 -0
- oscura/analyzers/protocols/parallel_bus/centronics.py +92 -0
- oscura/analyzers/protocols/parallel_bus/gpib.py +137 -0
- oscura/analyzers/protocols/spi.py +192 -95
- oscura/analyzers/protocols/swd.py +321 -167
- oscura/analyzers/protocols/uart.py +267 -125
- oscura/analyzers/protocols/usb.py +235 -131
- oscura/analyzers/side_channel/power.py +17 -12
- oscura/analyzers/signal/__init__.py +15 -0
- oscura/analyzers/signal/timing_analysis.py +1086 -0
- oscura/analyzers/signal_integrity/__init__.py +4 -1
- oscura/analyzers/signal_integrity/sparams.py +2 -19
- oscura/analyzers/spectral/chunked.py +129 -60
- oscura/analyzers/spectral/chunked_fft.py +300 -94
- oscura/analyzers/spectral/chunked_wavelet.py +100 -80
- oscura/analyzers/statistical/checksum.py +376 -217
- oscura/analyzers/statistical/classification.py +229 -107
- oscura/analyzers/statistical/entropy.py +78 -53
- oscura/analyzers/statistics/correlation.py +407 -211
- oscura/analyzers/statistics/outliers.py +2 -2
- oscura/analyzers/statistics/streaming.py +30 -5
- oscura/analyzers/validation.py +216 -101
- oscura/analyzers/waveform/measurements.py +9 -0
- oscura/analyzers/waveform/measurements_with_uncertainty.py +31 -15
- oscura/analyzers/waveform/spectral.py +500 -228
- oscura/api/__init__.py +31 -5
- oscura/api/dsl/__init__.py +582 -0
- oscura/{dsl → api/dsl}/commands.py +43 -76
- oscura/{dsl → api/dsl}/interpreter.py +26 -51
- oscura/{dsl → api/dsl}/parser.py +107 -77
- oscura/{dsl → api/dsl}/repl.py +2 -2
- oscura/api/dsl.py +1 -1
- oscura/{integrations → api/integrations}/__init__.py +1 -1
- oscura/{integrations → api/integrations}/llm.py +201 -102
- oscura/api/operators.py +3 -3
- oscura/api/optimization.py +144 -30
- oscura/api/rest_server.py +921 -0
- oscura/api/server/__init__.py +17 -0
- oscura/api/server/dashboard.py +850 -0
- oscura/api/server/static/README.md +34 -0
- oscura/api/server/templates/base.html +181 -0
- oscura/api/server/templates/export.html +120 -0
- oscura/api/server/templates/home.html +284 -0
- oscura/api/server/templates/protocols.html +58 -0
- oscura/api/server/templates/reports.html +43 -0
- oscura/api/server/templates/session_detail.html +89 -0
- oscura/api/server/templates/sessions.html +83 -0
- oscura/api/server/templates/waveforms.html +73 -0
- oscura/automotive/__init__.py +8 -1
- oscura/automotive/can/__init__.py +10 -0
- oscura/automotive/can/checksum.py +3 -1
- oscura/automotive/can/dbc_generator.py +590 -0
- oscura/automotive/can/message_wrapper.py +121 -74
- oscura/automotive/can/patterns.py +98 -21
- oscura/automotive/can/session.py +292 -56
- oscura/automotive/can/state_machine.py +6 -3
- oscura/automotive/can/stimulus_response.py +97 -75
- oscura/automotive/dbc/__init__.py +10 -2
- oscura/automotive/dbc/generator.py +84 -56
- oscura/automotive/dbc/parser.py +6 -6
- oscura/automotive/dtc/data.json +2763 -0
- oscura/automotive/dtc/database.py +2 -2
- oscura/automotive/flexray/__init__.py +31 -0
- oscura/automotive/flexray/analyzer.py +504 -0
- oscura/automotive/flexray/crc.py +185 -0
- oscura/automotive/flexray/fibex.py +449 -0
- oscura/automotive/j1939/__init__.py +45 -8
- oscura/automotive/j1939/analyzer.py +605 -0
- oscura/automotive/j1939/spns.py +326 -0
- oscura/automotive/j1939/transport.py +306 -0
- oscura/automotive/lin/__init__.py +47 -0
- oscura/automotive/lin/analyzer.py +612 -0
- oscura/automotive/loaders/blf.py +13 -2
- oscura/automotive/loaders/csv_can.py +143 -72
- oscura/automotive/loaders/dispatcher.py +50 -2
- oscura/automotive/loaders/mdf.py +86 -45
- oscura/automotive/loaders/pcap.py +111 -61
- oscura/automotive/uds/__init__.py +4 -0
- oscura/automotive/uds/analyzer.py +725 -0
- oscura/automotive/uds/decoder.py +140 -58
- oscura/automotive/uds/models.py +7 -1
- oscura/automotive/visualization.py +1 -1
- oscura/cli/analyze.py +348 -0
- oscura/cli/batch.py +142 -122
- oscura/cli/benchmark.py +275 -0
- oscura/cli/characterize.py +137 -82
- oscura/cli/compare.py +224 -131
- oscura/cli/completion.py +250 -0
- oscura/cli/config_cmd.py +361 -0
- oscura/cli/decode.py +164 -87
- oscura/cli/export.py +286 -0
- oscura/cli/main.py +115 -31
- oscura/{onboarding → cli/onboarding}/__init__.py +3 -3
- oscura/{onboarding → cli/onboarding}/help.py +80 -58
- oscura/{onboarding → cli/onboarding}/tutorials.py +97 -72
- oscura/{onboarding → cli/onboarding}/wizard.py +55 -36
- oscura/cli/progress.py +147 -0
- oscura/cli/shell.py +157 -135
- oscura/cli/validate_cmd.py +204 -0
- oscura/cli/visualize.py +158 -0
- oscura/convenience.py +125 -79
- oscura/core/__init__.py +4 -2
- oscura/core/backend_selector.py +3 -3
- oscura/core/cache.py +126 -15
- oscura/core/cancellation.py +1 -1
- oscura/{config → core/config}/__init__.py +20 -11
- oscura/{config → core/config}/defaults.py +1 -1
- oscura/{config → core/config}/loader.py +7 -5
- oscura/{config → core/config}/memory.py +5 -5
- oscura/{config → core/config}/migration.py +1 -1
- oscura/{config → core/config}/pipeline.py +99 -23
- oscura/{config → core/config}/preferences.py +1 -1
- oscura/{config → core/config}/protocol.py +3 -3
- oscura/{config → core/config}/schema.py +426 -272
- oscura/{config → core/config}/settings.py +1 -1
- oscura/{config → core/config}/thresholds.py +195 -153
- oscura/core/correlation.py +5 -6
- oscura/core/cross_domain.py +0 -2
- oscura/core/debug.py +9 -5
- oscura/{extensibility → core/extensibility}/docs.py +158 -70
- oscura/{extensibility → core/extensibility}/extensions.py +160 -76
- oscura/{extensibility → core/extensibility}/logging.py +1 -1
- oscura/{extensibility → core/extensibility}/measurements.py +1 -1
- oscura/{extensibility → core/extensibility}/plugins.py +1 -1
- oscura/{extensibility → core/extensibility}/templates.py +73 -3
- oscura/{extensibility → core/extensibility}/validation.py +1 -1
- oscura/core/gpu_backend.py +11 -7
- oscura/core/log_query.py +101 -11
- oscura/core/logging.py +126 -54
- oscura/core/logging_advanced.py +5 -5
- oscura/core/memory_limits.py +108 -70
- oscura/core/memory_monitor.py +2 -2
- oscura/core/memory_progress.py +7 -7
- oscura/core/memory_warnings.py +1 -1
- oscura/core/numba_backend.py +13 -13
- oscura/{plugins → core/plugins}/__init__.py +9 -9
- oscura/{plugins → core/plugins}/base.py +7 -7
- oscura/{plugins → core/plugins}/cli.py +3 -3
- oscura/{plugins → core/plugins}/discovery.py +186 -106
- oscura/{plugins → core/plugins}/lifecycle.py +1 -1
- oscura/{plugins → core/plugins}/manager.py +7 -7
- oscura/{plugins → core/plugins}/registry.py +3 -3
- oscura/{plugins → core/plugins}/versioning.py +1 -1
- oscura/core/progress.py +16 -1
- oscura/core/provenance.py +8 -2
- oscura/{schemas → core/schemas}/__init__.py +2 -2
- oscura/core/schemas/bus_configuration.json +322 -0
- oscura/core/schemas/device_mapping.json +182 -0
- oscura/core/schemas/packet_format.json +418 -0
- oscura/core/schemas/protocol_definition.json +363 -0
- oscura/core/types.py +4 -0
- oscura/core/uncertainty.py +3 -3
- oscura/correlation/__init__.py +52 -0
- oscura/correlation/multi_protocol.py +811 -0
- oscura/discovery/auto_decoder.py +117 -35
- oscura/discovery/comparison.py +191 -86
- oscura/discovery/quality_validator.py +155 -68
- oscura/discovery/signal_detector.py +196 -79
- oscura/export/__init__.py +18 -20
- oscura/export/kaitai_struct.py +513 -0
- oscura/export/scapy_layer.py +801 -0
- oscura/export/wireshark/README.md +15 -15
- oscura/export/wireshark/generator.py +1 -1
- oscura/export/wireshark/templates/dissector.lua.j2 +2 -2
- oscura/export/wireshark_dissector.py +746 -0
- oscura/guidance/wizard.py +207 -111
- oscura/hardware/__init__.py +19 -0
- oscura/{acquisition → hardware/acquisition}/__init__.py +4 -4
- oscura/{acquisition → hardware/acquisition}/file.py +2 -2
- oscura/{acquisition → hardware/acquisition}/hardware.py +7 -7
- oscura/{acquisition → hardware/acquisition}/saleae.py +15 -12
- oscura/{acquisition → hardware/acquisition}/socketcan.py +1 -1
- oscura/{acquisition → hardware/acquisition}/streaming.py +2 -2
- oscura/{acquisition → hardware/acquisition}/synthetic.py +3 -3
- oscura/{acquisition → hardware/acquisition}/visa.py +33 -11
- oscura/hardware/firmware/__init__.py +29 -0
- oscura/hardware/firmware/pattern_recognition.py +874 -0
- oscura/hardware/hal_detector.py +736 -0
- oscura/hardware/security/__init__.py +37 -0
- oscura/hardware/security/side_channel_detector.py +1126 -0
- oscura/inference/__init__.py +4 -0
- oscura/inference/active_learning/README.md +7 -7
- oscura/inference/active_learning/observation_table.py +4 -1
- oscura/inference/alignment.py +216 -123
- oscura/inference/bayesian.py +113 -33
- oscura/inference/crc_reverse.py +101 -55
- oscura/inference/logic.py +6 -2
- oscura/inference/message_format.py +342 -183
- oscura/inference/protocol.py +95 -44
- oscura/inference/protocol_dsl.py +180 -82
- oscura/inference/signal_intelligence.py +1439 -706
- oscura/inference/spectral.py +99 -57
- oscura/inference/state_machine.py +810 -158
- oscura/inference/stream.py +270 -110
- oscura/iot/__init__.py +34 -0
- oscura/iot/coap/__init__.py +32 -0
- oscura/iot/coap/analyzer.py +668 -0
- oscura/iot/coap/options.py +212 -0
- oscura/iot/lorawan/__init__.py +21 -0
- oscura/iot/lorawan/crypto.py +206 -0
- oscura/iot/lorawan/decoder.py +801 -0
- oscura/iot/lorawan/mac_commands.py +341 -0
- oscura/iot/mqtt/__init__.py +27 -0
- oscura/iot/mqtt/analyzer.py +999 -0
- oscura/iot/mqtt/properties.py +315 -0
- oscura/iot/zigbee/__init__.py +31 -0
- oscura/iot/zigbee/analyzer.py +615 -0
- oscura/iot/zigbee/security.py +153 -0
- oscura/iot/zigbee/zcl.py +349 -0
- oscura/jupyter/display.py +125 -45
- oscura/{exploratory → jupyter/exploratory}/__init__.py +8 -8
- oscura/{exploratory → jupyter/exploratory}/error_recovery.py +298 -141
- oscura/jupyter/exploratory/fuzzy.py +746 -0
- oscura/{exploratory → jupyter/exploratory}/fuzzy_advanced.py +258 -100
- oscura/{exploratory → jupyter/exploratory}/legacy.py +464 -242
- oscura/{exploratory → jupyter/exploratory}/parse.py +167 -145
- oscura/{exploratory → jupyter/exploratory}/recovery.py +119 -87
- oscura/jupyter/exploratory/sync.py +612 -0
- oscura/{exploratory → jupyter/exploratory}/unknown.py +299 -176
- oscura/jupyter/magic.py +4 -4
- oscura/{ui → jupyter/ui}/__init__.py +2 -2
- oscura/{ui → jupyter/ui}/formatters.py +3 -3
- oscura/{ui → jupyter/ui}/progressive_display.py +153 -82
- oscura/loaders/__init__.py +171 -63
- oscura/loaders/binary.py +88 -1
- oscura/loaders/chipwhisperer.py +153 -137
- oscura/loaders/configurable.py +208 -86
- oscura/loaders/csv_loader.py +458 -215
- oscura/loaders/hdf5_loader.py +278 -119
- oscura/loaders/lazy.py +87 -54
- oscura/loaders/mmap_loader.py +1 -1
- oscura/loaders/numpy_loader.py +253 -116
- oscura/loaders/pcap.py +226 -151
- oscura/loaders/rigol.py +110 -49
- oscura/loaders/sigrok.py +201 -78
- oscura/loaders/tdms.py +81 -58
- oscura/loaders/tektronix.py +291 -174
- oscura/loaders/touchstone.py +182 -87
- oscura/loaders/vcd.py +215 -117
- oscura/loaders/wav.py +155 -68
- oscura/reporting/__init__.py +9 -7
- oscura/reporting/analyze.py +352 -146
- oscura/reporting/argument_preparer.py +69 -14
- oscura/reporting/auto_report.py +97 -61
- oscura/reporting/batch.py +131 -58
- oscura/reporting/chart_selection.py +57 -45
- oscura/reporting/comparison.py +63 -17
- oscura/reporting/content/executive.py +76 -24
- oscura/reporting/core_formats/multi_format.py +11 -8
- oscura/reporting/engine.py +312 -158
- oscura/reporting/enhanced_reports.py +949 -0
- oscura/reporting/export.py +86 -43
- oscura/reporting/formatting/numbers.py +69 -42
- oscura/reporting/html.py +139 -58
- oscura/reporting/index.py +137 -65
- oscura/reporting/output.py +158 -67
- oscura/reporting/pdf.py +67 -102
- oscura/reporting/plots.py +191 -112
- oscura/reporting/sections.py +88 -47
- oscura/reporting/standards.py +104 -61
- oscura/reporting/summary_generator.py +75 -55
- oscura/reporting/tables.py +138 -54
- oscura/reporting/templates/enhanced/protocol_re.html +525 -0
- oscura/reporting/templates/index.md +13 -13
- oscura/sessions/__init__.py +14 -23
- oscura/sessions/base.py +3 -3
- oscura/sessions/blackbox.py +106 -10
- oscura/sessions/generic.py +2 -2
- oscura/sessions/legacy.py +783 -0
- oscura/side_channel/__init__.py +63 -0
- oscura/side_channel/dpa.py +1025 -0
- oscura/utils/__init__.py +15 -1
- oscura/utils/autodetect.py +1 -5
- oscura/utils/bitwise.py +118 -0
- oscura/{builders → utils/builders}/__init__.py +1 -1
- oscura/{comparison → utils/comparison}/__init__.py +6 -6
- oscura/{comparison → utils/comparison}/compare.py +202 -101
- oscura/{comparison → utils/comparison}/golden.py +83 -63
- oscura/{comparison → utils/comparison}/limits.py +313 -89
- oscura/{comparison → utils/comparison}/mask.py +151 -45
- oscura/{comparison → utils/comparison}/trace_diff.py +1 -1
- oscura/{comparison → utils/comparison}/visualization.py +147 -89
- oscura/{component → utils/component}/__init__.py +3 -3
- oscura/{component → utils/component}/impedance.py +122 -58
- oscura/{component → utils/component}/reactive.py +165 -168
- oscura/{component → utils/component}/transmission_line.py +3 -3
- oscura/{filtering → utils/filtering}/__init__.py +6 -6
- oscura/{filtering → utils/filtering}/base.py +1 -1
- oscura/{filtering → utils/filtering}/convenience.py +2 -2
- oscura/{filtering → utils/filtering}/design.py +169 -93
- oscura/{filtering → utils/filtering}/filters.py +2 -2
- oscura/{filtering → utils/filtering}/introspection.py +2 -2
- oscura/utils/geometry.py +31 -0
- oscura/utils/imports.py +184 -0
- oscura/utils/lazy.py +1 -1
- oscura/{math → utils/math}/__init__.py +2 -2
- oscura/{math → utils/math}/arithmetic.py +114 -48
- oscura/{math → utils/math}/interpolation.py +139 -106
- oscura/utils/memory.py +129 -66
- oscura/utils/memory_advanced.py +92 -9
- oscura/utils/memory_extensions.py +10 -8
- oscura/{optimization → utils/optimization}/__init__.py +1 -1
- oscura/{optimization → utils/optimization}/search.py +2 -2
- oscura/utils/performance/__init__.py +58 -0
- oscura/utils/performance/caching.py +889 -0
- oscura/utils/performance/lsh_clustering.py +333 -0
- oscura/utils/performance/memory_optimizer.py +699 -0
- oscura/utils/performance/optimizations.py +675 -0
- oscura/utils/performance/parallel.py +654 -0
- oscura/utils/performance/profiling.py +661 -0
- oscura/{pipeline → utils/pipeline}/base.py +1 -1
- oscura/{pipeline → utils/pipeline}/composition.py +11 -3
- oscura/{pipeline → utils/pipeline}/parallel.py +3 -2
- oscura/{pipeline → utils/pipeline}/pipeline.py +1 -1
- oscura/{pipeline → utils/pipeline}/reverse_engineering.py +412 -221
- oscura/{search → utils/search}/__init__.py +3 -3
- oscura/{search → utils/search}/anomaly.py +188 -58
- oscura/utils/search/context.py +294 -0
- oscura/{search → utils/search}/pattern.py +138 -10
- oscura/utils/serial.py +51 -0
- oscura/utils/storage/__init__.py +61 -0
- oscura/utils/storage/database.py +1166 -0
- oscura/{streaming → utils/streaming}/chunked.py +302 -143
- oscura/{streaming → utils/streaming}/progressive.py +1 -1
- oscura/{streaming → utils/streaming}/realtime.py +3 -2
- oscura/{triggering → utils/triggering}/__init__.py +6 -6
- oscura/{triggering → utils/triggering}/base.py +6 -6
- oscura/{triggering → utils/triggering}/edge.py +2 -2
- oscura/{triggering → utils/triggering}/pattern.py +2 -2
- oscura/{triggering → utils/triggering}/pulse.py +115 -74
- oscura/{triggering → utils/triggering}/window.py +2 -2
- oscura/utils/validation.py +32 -0
- oscura/validation/__init__.py +121 -0
- oscura/{compliance → validation/compliance}/__init__.py +5 -5
- oscura/{compliance → validation/compliance}/advanced.py +5 -5
- oscura/{compliance → validation/compliance}/masks.py +1 -1
- oscura/{compliance → validation/compliance}/reporting.py +127 -53
- oscura/{compliance → validation/compliance}/testing.py +114 -52
- oscura/validation/compliance_tests.py +915 -0
- oscura/validation/fuzzer.py +990 -0
- oscura/validation/grammar_tests.py +596 -0
- oscura/validation/grammar_validator.py +904 -0
- oscura/validation/hil_testing.py +977 -0
- oscura/{quality → validation/quality}/__init__.py +4 -4
- oscura/{quality → validation/quality}/ensemble.py +251 -171
- oscura/{quality → validation/quality}/explainer.py +3 -3
- oscura/{quality → validation/quality}/scoring.py +1 -1
- oscura/{quality → validation/quality}/warnings.py +4 -4
- oscura/validation/regression_suite.py +808 -0
- oscura/validation/replay.py +788 -0
- oscura/{testing → validation/testing}/__init__.py +2 -2
- oscura/{testing → validation/testing}/synthetic.py +5 -5
- oscura/visualization/__init__.py +9 -0
- oscura/visualization/accessibility.py +1 -1
- oscura/visualization/annotations.py +64 -67
- oscura/visualization/colors.py +7 -7
- oscura/visualization/digital.py +180 -81
- oscura/visualization/eye.py +236 -85
- oscura/visualization/interactive.py +320 -143
- oscura/visualization/jitter.py +587 -247
- oscura/visualization/layout.py +169 -134
- oscura/visualization/optimization.py +103 -52
- oscura/visualization/palettes.py +1 -1
- oscura/visualization/power.py +427 -211
- oscura/visualization/power_extended.py +626 -297
- oscura/visualization/presets.py +2 -0
- oscura/visualization/protocols.py +495 -181
- oscura/visualization/render.py +79 -63
- oscura/visualization/reverse_engineering.py +171 -124
- oscura/visualization/signal_integrity.py +460 -279
- oscura/visualization/specialized.py +190 -100
- oscura/visualization/spectral.py +670 -255
- oscura/visualization/thumbnails.py +166 -137
- oscura/visualization/waveform.py +150 -63
- oscura/workflows/__init__.py +3 -0
- oscura/{batch → workflows/batch}/__init__.py +5 -5
- oscura/{batch → workflows/batch}/advanced.py +150 -75
- oscura/workflows/batch/aggregate.py +531 -0
- oscura/workflows/batch/analyze.py +236 -0
- oscura/{batch → workflows/batch}/logging.py +2 -2
- oscura/{batch → workflows/batch}/metrics.py +1 -1
- oscura/workflows/complete_re.py +1144 -0
- oscura/workflows/compliance.py +44 -54
- oscura/workflows/digital.py +197 -51
- oscura/workflows/legacy/__init__.py +12 -0
- oscura/{workflow → workflows/legacy}/dag.py +4 -1
- oscura/workflows/multi_trace.py +9 -9
- oscura/workflows/power.py +42 -62
- oscura/workflows/protocol.py +82 -49
- oscura/workflows/reverse_engineering.py +351 -150
- oscura/workflows/signal_integrity.py +157 -82
- oscura-0.6.0.dist-info/METADATA +643 -0
- oscura-0.6.0.dist-info/RECORD +590 -0
- oscura/analyzers/digital/ic_database.py +0 -498
- oscura/analyzers/digital/timing_paths.py +0 -339
- oscura/analyzers/digital/vintage.py +0 -377
- oscura/analyzers/digital/vintage_result.py +0 -148
- oscura/analyzers/protocols/parallel_bus.py +0 -449
- oscura/batch/aggregate.py +0 -300
- oscura/batch/analyze.py +0 -139
- oscura/dsl/__init__.py +0 -73
- oscura/exceptions.py +0 -59
- oscura/exploratory/fuzzy.py +0 -513
- oscura/exploratory/sync.py +0 -384
- oscura/export/wavedrom.py +0 -430
- oscura/exporters/__init__.py +0 -94
- oscura/exporters/csv.py +0 -303
- oscura/exporters/exporters.py +0 -44
- oscura/exporters/hdf5.py +0 -217
- oscura/exporters/html_export.py +0 -701
- oscura/exporters/json_export.py +0 -338
- oscura/exporters/markdown_export.py +0 -367
- oscura/exporters/matlab_export.py +0 -354
- oscura/exporters/npz_export.py +0 -219
- oscura/exporters/spice_export.py +0 -210
- oscura/exporters/vintage_logic_csv.py +0 -247
- oscura/reporting/vintage_logic_report.py +0 -523
- oscura/search/context.py +0 -149
- oscura/session/__init__.py +0 -34
- oscura/session/annotations.py +0 -289
- oscura/session/history.py +0 -313
- oscura/session/session.py +0 -520
- oscura/visualization/digital_advanced.py +0 -718
- oscura/visualization/figure_manager.py +0 -156
- oscura/workflow/__init__.py +0 -13
- oscura-0.5.0.dist-info/METADATA +0 -407
- oscura-0.5.0.dist-info/RECORD +0 -486
- /oscura/core/{config.py → config/legacy.py} +0 -0
- /oscura/{extensibility → core/extensibility}/__init__.py +0 -0
- /oscura/{extensibility → core/extensibility}/registry.py +0 -0
- /oscura/{plugins → core/plugins}/isolation.py +0 -0
- /oscura/{builders → utils/builders}/signal_builder.py +0 -0
- /oscura/{optimization → utils/optimization}/parallel.py +0 -0
- /oscura/{pipeline → utils/pipeline}/__init__.py +0 -0
- /oscura/{streaming → utils/streaming}/__init__.py +0 -0
- {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/WHEEL +0 -0
- {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/entry_points.txt +0 -0
- {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
|
+
]
|