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