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,874 @@
|
|
|
1
|
+
"""Firmware pattern recognition for binary analysis.
|
|
2
|
+
|
|
3
|
+
This module provides comprehensive firmware analysis capabilities including:
|
|
4
|
+
- Function boundary detection using architecture-specific patterns
|
|
5
|
+
- Architecture fingerprinting (ARM Thumb/ARM, x86, MIPS)
|
|
6
|
+
- String and data region identification
|
|
7
|
+
- Interrupt vector table detection
|
|
8
|
+
- Compiler signature detection
|
|
9
|
+
|
|
10
|
+
Example:
|
|
11
|
+
>>> from oscura.hardware.firmware.pattern_recognition import FirmwarePatternRecognizer
|
|
12
|
+
>>> with open("firmware.bin", "rb") as f:
|
|
13
|
+
... data = f.read()
|
|
14
|
+
>>> recognizer = FirmwarePatternRecognizer()
|
|
15
|
+
>>> result = recognizer.analyze(data, base_address=0x08000000)
|
|
16
|
+
>>> print(f"Architecture: {result.detected_architecture}")
|
|
17
|
+
>>> print(f"Functions: {len(result.functions)}")
|
|
18
|
+
>>> for func in result.functions[:5]:
|
|
19
|
+
... print(f" {func.address:08X}: {func.name or 'unknown'} ({func.confidence:.2f})")
|
|
20
|
+
|
|
21
|
+
References:
|
|
22
|
+
ARM Architecture Reference Manual (ARMv7-M)
|
|
23
|
+
Intel 64 and IA-32 Architectures Software Developer's Manual
|
|
24
|
+
MIPS Architecture For Programmers
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import json
|
|
30
|
+
import struct
|
|
31
|
+
from dataclasses import dataclass, field
|
|
32
|
+
from enum import Enum
|
|
33
|
+
from typing import TYPE_CHECKING, ClassVar
|
|
34
|
+
|
|
35
|
+
import numpy as np
|
|
36
|
+
|
|
37
|
+
if TYPE_CHECKING:
|
|
38
|
+
from collections.abc import Sequence
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class Architecture(Enum):
|
|
42
|
+
"""Detected CPU architecture."""
|
|
43
|
+
|
|
44
|
+
UNKNOWN = "unknown"
|
|
45
|
+
ARM_THUMB = "arm_thumb"
|
|
46
|
+
ARM_ARM = "arm_arm"
|
|
47
|
+
X86 = "x86"
|
|
48
|
+
X86_64 = "x86_64"
|
|
49
|
+
MIPS = "mips"
|
|
50
|
+
MIPS64 = "mips64"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class CompilerSignature(Enum):
|
|
54
|
+
"""Detected compiler toolchain."""
|
|
55
|
+
|
|
56
|
+
UNKNOWN = "unknown"
|
|
57
|
+
GCC = "gcc"
|
|
58
|
+
IAR = "iar"
|
|
59
|
+
KEIL = "keil"
|
|
60
|
+
LLVM = "llvm"
|
|
61
|
+
MSVC = "msvc"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass
|
|
65
|
+
class Function:
|
|
66
|
+
"""Detected function in firmware.
|
|
67
|
+
|
|
68
|
+
Attributes:
|
|
69
|
+
address: Function start address (absolute or offset from base)
|
|
70
|
+
size: Function size in bytes (0 if unknown)
|
|
71
|
+
name: Function name if identifiable (e.g., "reset_handler")
|
|
72
|
+
confidence: Confidence score 0.0-1.0
|
|
73
|
+
architecture: Detected architecture for this function
|
|
74
|
+
metadata: Additional function information
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
address: int
|
|
78
|
+
size: int = 0
|
|
79
|
+
name: str | None = None
|
|
80
|
+
confidence: float = 0.0
|
|
81
|
+
architecture: Architecture = Architecture.UNKNOWN
|
|
82
|
+
metadata: dict[str, str | int | float] = field(default_factory=dict)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass
|
|
86
|
+
class StringTable:
|
|
87
|
+
"""Detected string table or string region.
|
|
88
|
+
|
|
89
|
+
Attributes:
|
|
90
|
+
address: Start address of string table
|
|
91
|
+
size: Size in bytes
|
|
92
|
+
strings: List of decoded strings
|
|
93
|
+
encoding: String encoding (utf-8, ascii, utf-16le)
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
address: int
|
|
97
|
+
size: int
|
|
98
|
+
strings: list[str]
|
|
99
|
+
encoding: str = "utf-8"
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@dataclass
|
|
103
|
+
class InterruptVector:
|
|
104
|
+
"""Detected interrupt vector table entry.
|
|
105
|
+
|
|
106
|
+
Attributes:
|
|
107
|
+
index: Vector index (0 = reset/stack pointer)
|
|
108
|
+
address: Handler address (or stack pointer for index 0)
|
|
109
|
+
name: Vector name if known (e.g., "SysTick_Handler")
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
index: int
|
|
113
|
+
address: int
|
|
114
|
+
name: str | None = None
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@dataclass
|
|
118
|
+
class FirmwareAnalysisResult:
|
|
119
|
+
"""Complete firmware analysis result.
|
|
120
|
+
|
|
121
|
+
Attributes:
|
|
122
|
+
detected_architecture: Most likely CPU architecture
|
|
123
|
+
functions: List of detected functions
|
|
124
|
+
string_tables: List of detected string tables
|
|
125
|
+
interrupt_vectors: List of detected interrupt vectors
|
|
126
|
+
compiler_signature: Detected compiler toolchain
|
|
127
|
+
base_address: Base address used for analysis
|
|
128
|
+
firmware_size: Total firmware size in bytes
|
|
129
|
+
code_regions: List of (start, size) tuples for code regions
|
|
130
|
+
data_regions: List of (start, size) tuples for data regions
|
|
131
|
+
metadata: Additional analysis metadata
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
detected_architecture: Architecture
|
|
135
|
+
functions: list[Function]
|
|
136
|
+
string_tables: list[StringTable]
|
|
137
|
+
interrupt_vectors: list[InterruptVector]
|
|
138
|
+
compiler_signature: CompilerSignature = CompilerSignature.UNKNOWN
|
|
139
|
+
base_address: int = 0
|
|
140
|
+
firmware_size: int = 0
|
|
141
|
+
code_regions: list[tuple[int, int]] = field(default_factory=list)
|
|
142
|
+
data_regions: list[tuple[int, int]] = field(default_factory=list)
|
|
143
|
+
metadata: dict[str, str | int | float] = field(default_factory=dict)
|
|
144
|
+
|
|
145
|
+
def to_dict(self) -> dict[str, object]:
|
|
146
|
+
"""Export analysis result to dictionary.
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
Dictionary representation suitable for JSON export
|
|
150
|
+
"""
|
|
151
|
+
return {
|
|
152
|
+
"detected_architecture": self.detected_architecture.value,
|
|
153
|
+
"base_address": hex(self.base_address),
|
|
154
|
+
"firmware_size": self.firmware_size,
|
|
155
|
+
"compiler_signature": self.compiler_signature.value,
|
|
156
|
+
"functions": [
|
|
157
|
+
{
|
|
158
|
+
"address": hex(f.address),
|
|
159
|
+
"size": f.size,
|
|
160
|
+
"name": f.name,
|
|
161
|
+
"confidence": f.confidence,
|
|
162
|
+
"architecture": f.architecture.value,
|
|
163
|
+
"metadata": f.metadata,
|
|
164
|
+
}
|
|
165
|
+
for f in self.functions
|
|
166
|
+
],
|
|
167
|
+
"string_tables": [
|
|
168
|
+
{
|
|
169
|
+
"address": hex(s.address),
|
|
170
|
+
"size": s.size,
|
|
171
|
+
"encoding": s.encoding,
|
|
172
|
+
"strings": s.strings,
|
|
173
|
+
}
|
|
174
|
+
for s in self.string_tables
|
|
175
|
+
],
|
|
176
|
+
"interrupt_vectors": [
|
|
177
|
+
{"index": v.index, "address": hex(v.address), "name": v.name}
|
|
178
|
+
for v in self.interrupt_vectors
|
|
179
|
+
],
|
|
180
|
+
"code_regions": [[hex(addr), size] for addr, size in self.code_regions],
|
|
181
|
+
"data_regions": [[hex(addr), size] for addr, size in self.data_regions],
|
|
182
|
+
"metadata": self.metadata,
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
def to_json(self, indent: int = 2) -> str:
|
|
186
|
+
"""Export analysis result to JSON.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
indent: JSON indentation level
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
JSON string representation
|
|
193
|
+
"""
|
|
194
|
+
return json.dumps(self.to_dict(), indent=indent)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class FirmwarePatternRecognizer:
|
|
198
|
+
"""Firmware pattern recognition and analysis.
|
|
199
|
+
|
|
200
|
+
This class provides comprehensive firmware analysis including function
|
|
201
|
+
detection, architecture fingerprinting, and data region identification.
|
|
202
|
+
|
|
203
|
+
Example:
|
|
204
|
+
>>> recognizer = FirmwarePatternRecognizer()
|
|
205
|
+
>>> result = recognizer.analyze(firmware_data, base_address=0x08000000)
|
|
206
|
+
>>> print(result.to_json())
|
|
207
|
+
"""
|
|
208
|
+
|
|
209
|
+
# ARM Thumb function prologue patterns (16-bit instructions)
|
|
210
|
+
ARM_THUMB_PROLOGUES: ClassVar[list[bytes]] = [
|
|
211
|
+
b"\xb5\x00", # PUSH {lr} (0xb500)
|
|
212
|
+
b"\xb5\x10", # PUSH {r4, lr}
|
|
213
|
+
b"\xb5\x30", # PUSH {r4, r5, lr}
|
|
214
|
+
b"\xb5\x70", # PUSH {r4, r5, r6, lr}
|
|
215
|
+
b"\xb5\xf0", # PUSH {r4, r5, r6, r7, lr}
|
|
216
|
+
b"\xb5\x80", # PUSH {r7, lr}
|
|
217
|
+
]
|
|
218
|
+
|
|
219
|
+
# ARM Thumb function epilogue patterns
|
|
220
|
+
ARM_THUMB_EPILOGUES: ClassVar[list[bytes]] = [
|
|
221
|
+
b"\xbd\x00", # POP {pc} (0xbd00)
|
|
222
|
+
b"\xbd\x10", # POP {r4, pc}
|
|
223
|
+
b"\xbd\x30", # POP {r4, r5, pc}
|
|
224
|
+
b"\xbd\x70", # POP {r4, r5, r6, pc}
|
|
225
|
+
b"\xbd\xf0", # POP {r4, r5, r6, r7, pc}
|
|
226
|
+
b"\xbd\x80", # POP {r7, pc}
|
|
227
|
+
b"\x70\x47", # BX lr (0x4770)
|
|
228
|
+
]
|
|
229
|
+
|
|
230
|
+
# x86 function prologue patterns
|
|
231
|
+
X86_PROLOGUES: ClassVar[list[bytes]] = [
|
|
232
|
+
b"\x55\x89\xe5", # PUSH ebp; MOV ebp, esp
|
|
233
|
+
b"\x55\x8b\xec", # PUSH ebp; MOV ebp, esp (alternate)
|
|
234
|
+
b"\x48\x89\x5c\x24", # x64: MOV [rsp+X], rbx
|
|
235
|
+
b"\x48\x83\xec", # x64: SUB rsp, X
|
|
236
|
+
]
|
|
237
|
+
|
|
238
|
+
# x86 function epilogue patterns
|
|
239
|
+
X86_EPILOGUES: ClassVar[list[bytes]] = [
|
|
240
|
+
b"\xc9\xc3", # LEAVE; RET
|
|
241
|
+
b"\x5d\xc3", # POP ebp; RET
|
|
242
|
+
b"\xc3", # RET (simple)
|
|
243
|
+
b"\x48\x83\xc4", # x64: ADD rsp, X
|
|
244
|
+
]
|
|
245
|
+
|
|
246
|
+
# ARM Cortex-M vector table standard handlers
|
|
247
|
+
CORTEX_M_VECTORS: ClassVar[dict[int, str]] = {
|
|
248
|
+
1: "Reset_Handler",
|
|
249
|
+
2: "NMI_Handler",
|
|
250
|
+
3: "HardFault_Handler",
|
|
251
|
+
4: "MemManage_Handler",
|
|
252
|
+
5: "BusFault_Handler",
|
|
253
|
+
6: "UsageFault_Handler",
|
|
254
|
+
11: "SVC_Handler",
|
|
255
|
+
12: "DebugMon_Handler",
|
|
256
|
+
14: "PendSV_Handler",
|
|
257
|
+
15: "SysTick_Handler",
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
def analyze(
|
|
261
|
+
self,
|
|
262
|
+
firmware_data: bytes | Sequence[int],
|
|
263
|
+
base_address: int = 0,
|
|
264
|
+
architecture_hint: Architecture | None = None,
|
|
265
|
+
) -> FirmwareAnalysisResult:
|
|
266
|
+
"""Analyze firmware binary for patterns and structures.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
firmware_data: Raw firmware binary data
|
|
270
|
+
base_address: Base address for firmware (default 0)
|
|
271
|
+
architecture_hint: Optional architecture hint to guide analysis
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
Complete firmware analysis result
|
|
275
|
+
|
|
276
|
+
Raises:
|
|
277
|
+
ValueError: If firmware_data is empty or invalid
|
|
278
|
+
"""
|
|
279
|
+
if not firmware_data:
|
|
280
|
+
raise ValueError("Firmware data cannot be empty")
|
|
281
|
+
|
|
282
|
+
# Convert to bytes if needed
|
|
283
|
+
data: bytes
|
|
284
|
+
if isinstance(firmware_data, bytes):
|
|
285
|
+
data = firmware_data
|
|
286
|
+
else:
|
|
287
|
+
data = bytes(firmware_data)
|
|
288
|
+
|
|
289
|
+
# Detect architecture
|
|
290
|
+
if architecture_hint is None:
|
|
291
|
+
detected_arch = self._detect_architecture(data, base_address)
|
|
292
|
+
else:
|
|
293
|
+
detected_arch = architecture_hint
|
|
294
|
+
|
|
295
|
+
# Detect functions
|
|
296
|
+
functions = self._detect_functions(data, base_address, detected_arch)
|
|
297
|
+
|
|
298
|
+
# Detect string tables
|
|
299
|
+
string_tables = self._detect_string_tables(data, base_address)
|
|
300
|
+
|
|
301
|
+
# Detect interrupt vectors
|
|
302
|
+
interrupt_vectors = self._detect_interrupt_vectors(data, base_address, detected_arch)
|
|
303
|
+
|
|
304
|
+
# Detect compiler signature
|
|
305
|
+
compiler_sig = self._detect_compiler(data)
|
|
306
|
+
|
|
307
|
+
# Classify code vs data regions
|
|
308
|
+
code_regions, data_regions = self._classify_regions(data, base_address, detected_arch)
|
|
309
|
+
|
|
310
|
+
return FirmwareAnalysisResult(
|
|
311
|
+
detected_architecture=detected_arch,
|
|
312
|
+
functions=functions,
|
|
313
|
+
string_tables=string_tables,
|
|
314
|
+
interrupt_vectors=interrupt_vectors,
|
|
315
|
+
compiler_signature=compiler_sig,
|
|
316
|
+
base_address=base_address,
|
|
317
|
+
firmware_size=len(data),
|
|
318
|
+
code_regions=code_regions,
|
|
319
|
+
data_regions=data_regions,
|
|
320
|
+
metadata={
|
|
321
|
+
"function_count": len(functions),
|
|
322
|
+
"string_count": sum(len(st.strings) for st in string_tables),
|
|
323
|
+
"vector_count": len(interrupt_vectors),
|
|
324
|
+
},
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
def _detect_architecture(self, firmware_data: bytes, base_address: int) -> Architecture:
|
|
328
|
+
"""Detect CPU architecture from binary patterns.
|
|
329
|
+
|
|
330
|
+
Args:
|
|
331
|
+
firmware_data: Raw firmware binary
|
|
332
|
+
base_address: Base address for firmware
|
|
333
|
+
|
|
334
|
+
Returns:
|
|
335
|
+
Detected architecture
|
|
336
|
+
"""
|
|
337
|
+
# Check for ARM Thumb (16-bit instruction alignment)
|
|
338
|
+
thumb_score = self._score_arm_thumb(firmware_data)
|
|
339
|
+
|
|
340
|
+
# Check for ARM (32-bit instruction alignment)
|
|
341
|
+
arm_score = self._score_arm_arm(firmware_data)
|
|
342
|
+
|
|
343
|
+
# Check for x86
|
|
344
|
+
x86_score = self._score_x86(firmware_data)
|
|
345
|
+
|
|
346
|
+
# Check for MIPS
|
|
347
|
+
mips_score = self._score_mips(firmware_data)
|
|
348
|
+
|
|
349
|
+
# Determine architecture based on scores
|
|
350
|
+
scores = {
|
|
351
|
+
Architecture.ARM_THUMB: thumb_score,
|
|
352
|
+
Architecture.ARM_ARM: arm_score,
|
|
353
|
+
Architecture.X86: x86_score,
|
|
354
|
+
Architecture.MIPS: mips_score,
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
max_arch = max(scores, key=lambda a: scores[a])
|
|
358
|
+
if scores[max_arch] > 0.3: # Minimum confidence threshold
|
|
359
|
+
return max_arch
|
|
360
|
+
|
|
361
|
+
return Architecture.UNKNOWN
|
|
362
|
+
|
|
363
|
+
def _score_arm_thumb(self, firmware_data: bytes) -> float:
|
|
364
|
+
"""Score likelihood of ARM Thumb architecture.
|
|
365
|
+
|
|
366
|
+
Args:
|
|
367
|
+
firmware_data: Raw firmware binary
|
|
368
|
+
|
|
369
|
+
Returns:
|
|
370
|
+
Confidence score 0.0-1.0
|
|
371
|
+
"""
|
|
372
|
+
if len(firmware_data) < 32:
|
|
373
|
+
return 0.0
|
|
374
|
+
|
|
375
|
+
score = 0.0
|
|
376
|
+
score += self._score_thumb_prologues(firmware_data)
|
|
377
|
+
score += self._score_thumb_epilogues(firmware_data)
|
|
378
|
+
score += self._score_thumb_vector_table(firmware_data)
|
|
379
|
+
|
|
380
|
+
return min(score, 1.0)
|
|
381
|
+
|
|
382
|
+
def _score_thumb_prologues(self, firmware_data: bytes) -> float:
|
|
383
|
+
"""Score Thumb prologue patterns."""
|
|
384
|
+
prologue_count = sum(firmware_data.count(pattern) for pattern in self.ARM_THUMB_PROLOGUES)
|
|
385
|
+
return min(prologue_count / 10.0, 0.4) if prologue_count > 0 else 0.0
|
|
386
|
+
|
|
387
|
+
def _score_thumb_epilogues(self, firmware_data: bytes) -> float:
|
|
388
|
+
"""Score Thumb epilogue patterns."""
|
|
389
|
+
epilogue_count = sum(firmware_data.count(pattern) for pattern in self.ARM_THUMB_EPILOGUES)
|
|
390
|
+
return min(epilogue_count / 10.0, 0.4) if epilogue_count > 0 else 0.0
|
|
391
|
+
|
|
392
|
+
def _score_thumb_vector_table(self, firmware_data: bytes) -> float:
|
|
393
|
+
"""Score Thumb LSB bit in vector table."""
|
|
394
|
+
if len(firmware_data) < 64:
|
|
395
|
+
return 0.0
|
|
396
|
+
|
|
397
|
+
try:
|
|
398
|
+
thumb_bit_count = 0
|
|
399
|
+
valid_addresses = 0
|
|
400
|
+
for i in range(1, min(16, len(firmware_data) // 4)):
|
|
401
|
+
word = struct.unpack("<I", firmware_data[i * 4 : i * 4 + 4])[0]
|
|
402
|
+
if word != 0 and word != 0xFFFFFFFF:
|
|
403
|
+
valid_addresses += 1
|
|
404
|
+
if word & 1:
|
|
405
|
+
thumb_bit_count += 1
|
|
406
|
+
|
|
407
|
+
if valid_addresses >= 3 and thumb_bit_count >= valid_addresses // 2:
|
|
408
|
+
return 0.3
|
|
409
|
+
except struct.error:
|
|
410
|
+
pass
|
|
411
|
+
|
|
412
|
+
return 0.0
|
|
413
|
+
|
|
414
|
+
def _score_arm_arm(self, firmware_data: bytes) -> float:
|
|
415
|
+
"""Score likelihood of ARM (32-bit) architecture.
|
|
416
|
+
|
|
417
|
+
Args:
|
|
418
|
+
firmware_data: Raw firmware binary
|
|
419
|
+
|
|
420
|
+
Returns:
|
|
421
|
+
Confidence score 0.0-1.0
|
|
422
|
+
"""
|
|
423
|
+
if len(firmware_data) < 32:
|
|
424
|
+
return 0.0
|
|
425
|
+
|
|
426
|
+
score = 0.0
|
|
427
|
+
|
|
428
|
+
# ARM instructions are 32-bit aligned and often have condition codes
|
|
429
|
+
# Check for common ARM instruction patterns (simplified)
|
|
430
|
+
arm_patterns = [
|
|
431
|
+
b"\x1e\xff\x2f\xe1", # BX lr (0xe12fff1e)
|
|
432
|
+
b"\x00\x00\xa0\xe3", # MOV r0, #0
|
|
433
|
+
]
|
|
434
|
+
|
|
435
|
+
for pattern in arm_patterns:
|
|
436
|
+
count = firmware_data.count(pattern)
|
|
437
|
+
if count > 0:
|
|
438
|
+
score += min(count / 50.0, 0.3)
|
|
439
|
+
|
|
440
|
+
return min(score, 1.0)
|
|
441
|
+
|
|
442
|
+
def _score_x86(self, firmware_data: bytes) -> float:
|
|
443
|
+
"""Score likelihood of x86 architecture.
|
|
444
|
+
|
|
445
|
+
Args:
|
|
446
|
+
firmware_data: Raw firmware binary
|
|
447
|
+
|
|
448
|
+
Returns:
|
|
449
|
+
Confidence score 0.0-1.0
|
|
450
|
+
"""
|
|
451
|
+
if len(firmware_data) < 32:
|
|
452
|
+
return 0.0
|
|
453
|
+
|
|
454
|
+
score = 0.0
|
|
455
|
+
|
|
456
|
+
# Check for x86 prologue patterns
|
|
457
|
+
prologue_count = 0
|
|
458
|
+
for pattern in self.X86_PROLOGUES:
|
|
459
|
+
prologue_count += firmware_data.count(pattern)
|
|
460
|
+
if prologue_count > 0:
|
|
461
|
+
score += min(prologue_count / 5.0, 0.5)
|
|
462
|
+
|
|
463
|
+
# Check for x86 epilogue patterns
|
|
464
|
+
epilogue_count = 0
|
|
465
|
+
for pattern in self.X86_EPILOGUES:
|
|
466
|
+
epilogue_count += firmware_data.count(pattern)
|
|
467
|
+
if epilogue_count > 0:
|
|
468
|
+
score += min(epilogue_count / 5.0, 0.5)
|
|
469
|
+
|
|
470
|
+
return min(score, 1.0)
|
|
471
|
+
|
|
472
|
+
def _score_mips(self, firmware_data: bytes) -> float:
|
|
473
|
+
"""Score likelihood of MIPS architecture.
|
|
474
|
+
|
|
475
|
+
Args:
|
|
476
|
+
firmware_data: Raw firmware binary
|
|
477
|
+
|
|
478
|
+
Returns:
|
|
479
|
+
Confidence score 0.0-1.0
|
|
480
|
+
"""
|
|
481
|
+
if len(firmware_data) < 32:
|
|
482
|
+
return 0.0
|
|
483
|
+
|
|
484
|
+
# MIPS instructions are 32-bit aligned
|
|
485
|
+
# Check for common MIPS patterns (simplified)
|
|
486
|
+
score = 0.0
|
|
487
|
+
|
|
488
|
+
# JR ra (return) - 0x03e00008
|
|
489
|
+
jr_ra_count = firmware_data.count(b"\x03\xe0\x00\x08")
|
|
490
|
+
if jr_ra_count > 0:
|
|
491
|
+
score += min(jr_ra_count / 50.0, 0.5)
|
|
492
|
+
|
|
493
|
+
return min(score, 1.0)
|
|
494
|
+
|
|
495
|
+
def _detect_functions(
|
|
496
|
+
self, firmware_data: bytes, base_address: int, architecture: Architecture
|
|
497
|
+
) -> list[Function]:
|
|
498
|
+
"""Detect function boundaries using pattern matching.
|
|
499
|
+
|
|
500
|
+
Args:
|
|
501
|
+
firmware_data: Raw firmware binary
|
|
502
|
+
base_address: Base address for firmware
|
|
503
|
+
architecture: Detected architecture
|
|
504
|
+
|
|
505
|
+
Returns:
|
|
506
|
+
List of detected functions
|
|
507
|
+
"""
|
|
508
|
+
functions: list[Function] = []
|
|
509
|
+
|
|
510
|
+
if architecture == Architecture.ARM_THUMB:
|
|
511
|
+
functions = self._detect_arm_thumb_functions(firmware_data, base_address)
|
|
512
|
+
elif architecture == Architecture.X86:
|
|
513
|
+
functions = self._detect_x86_functions(firmware_data, base_address)
|
|
514
|
+
elif architecture == Architecture.ARM_ARM:
|
|
515
|
+
functions = self._detect_arm_functions(firmware_data, base_address)
|
|
516
|
+
|
|
517
|
+
return functions
|
|
518
|
+
|
|
519
|
+
def _detect_arm_thumb_functions(
|
|
520
|
+
self, firmware_data: bytes, base_address: int
|
|
521
|
+
) -> list[Function]:
|
|
522
|
+
"""Detect ARM Thumb function boundaries.
|
|
523
|
+
|
|
524
|
+
Args:
|
|
525
|
+
firmware_data: Raw firmware binary
|
|
526
|
+
base_address: Base address for firmware
|
|
527
|
+
|
|
528
|
+
Returns:
|
|
529
|
+
List of detected functions
|
|
530
|
+
"""
|
|
531
|
+
functions: list[Function] = []
|
|
532
|
+
|
|
533
|
+
# Find all prologue patterns
|
|
534
|
+
prologues: list[tuple[int, bytes]] = []
|
|
535
|
+
for pattern in self.ARM_THUMB_PROLOGUES:
|
|
536
|
+
offset = 0
|
|
537
|
+
while True:
|
|
538
|
+
idx = firmware_data.find(pattern, offset)
|
|
539
|
+
if idx == -1:
|
|
540
|
+
break
|
|
541
|
+
# ARM Thumb requires 2-byte alignment
|
|
542
|
+
if idx % 2 == 0:
|
|
543
|
+
prologues.append((idx, pattern))
|
|
544
|
+
offset = idx + 1
|
|
545
|
+
|
|
546
|
+
# Find all epilogue patterns
|
|
547
|
+
epilogues: set[int] = set()
|
|
548
|
+
for pattern in self.ARM_THUMB_EPILOGUES:
|
|
549
|
+
offset = 0
|
|
550
|
+
while True:
|
|
551
|
+
idx = firmware_data.find(pattern, offset)
|
|
552
|
+
if idx == -1:
|
|
553
|
+
break
|
|
554
|
+
if idx % 2 == 0:
|
|
555
|
+
epilogues.add(idx)
|
|
556
|
+
offset = idx + 1
|
|
557
|
+
|
|
558
|
+
# Sort prologues by address
|
|
559
|
+
prologues.sort(key=lambda x: x[0])
|
|
560
|
+
|
|
561
|
+
# Match prologues with epilogues
|
|
562
|
+
for i, (prologue_addr, pattern) in enumerate(prologues):
|
|
563
|
+
# Find next epilogue after this prologue
|
|
564
|
+
size = 0
|
|
565
|
+
confidence = 0.5 # Base confidence for pattern match
|
|
566
|
+
|
|
567
|
+
# Find closest epilogue
|
|
568
|
+
matching_epilogues = [e for e in epilogues if e > prologue_addr]
|
|
569
|
+
if matching_epilogues:
|
|
570
|
+
epilogue_addr = min(matching_epilogues)
|
|
571
|
+
size = epilogue_addr - prologue_addr + 2
|
|
572
|
+
confidence = 0.7 # Higher confidence with epilogue match
|
|
573
|
+
|
|
574
|
+
# Check if next prologue is too close (avoid false positives)
|
|
575
|
+
if i + 1 < len(prologues):
|
|
576
|
+
next_prologue = prologues[i + 1][0]
|
|
577
|
+
if size == 0 or next_prologue < prologue_addr + size:
|
|
578
|
+
size = next_prologue - prologue_addr
|
|
579
|
+
|
|
580
|
+
# Create function entry
|
|
581
|
+
func = Function(
|
|
582
|
+
address=base_address + prologue_addr,
|
|
583
|
+
size=size,
|
|
584
|
+
confidence=confidence,
|
|
585
|
+
architecture=Architecture.ARM_THUMB,
|
|
586
|
+
metadata={"prologue_pattern": pattern.hex()},
|
|
587
|
+
)
|
|
588
|
+
functions.append(func)
|
|
589
|
+
|
|
590
|
+
return functions
|
|
591
|
+
|
|
592
|
+
def _detect_x86_functions(self, firmware_data: bytes, base_address: int) -> list[Function]:
|
|
593
|
+
"""Detect x86 function boundaries.
|
|
594
|
+
|
|
595
|
+
Args:
|
|
596
|
+
firmware_data: Raw firmware binary
|
|
597
|
+
base_address: Base address for firmware
|
|
598
|
+
|
|
599
|
+
Returns:
|
|
600
|
+
List of detected functions
|
|
601
|
+
"""
|
|
602
|
+
functions: list[Function] = []
|
|
603
|
+
|
|
604
|
+
# Find all prologue patterns
|
|
605
|
+
prologues: list[tuple[int, bytes]] = []
|
|
606
|
+
for pattern in self.X86_PROLOGUES:
|
|
607
|
+
offset = 0
|
|
608
|
+
while True:
|
|
609
|
+
idx = firmware_data.find(pattern, offset)
|
|
610
|
+
if idx == -1:
|
|
611
|
+
break
|
|
612
|
+
prologues.append((idx, pattern))
|
|
613
|
+
offset = idx + 1
|
|
614
|
+
|
|
615
|
+
prologues.sort(key=lambda x: x[0])
|
|
616
|
+
|
|
617
|
+
for prologue_addr, pattern in prologues:
|
|
618
|
+
func = Function(
|
|
619
|
+
address=base_address + prologue_addr,
|
|
620
|
+
size=0,
|
|
621
|
+
confidence=0.6,
|
|
622
|
+
architecture=Architecture.X86,
|
|
623
|
+
metadata={"prologue_pattern": pattern.hex()},
|
|
624
|
+
)
|
|
625
|
+
functions.append(func)
|
|
626
|
+
|
|
627
|
+
return functions
|
|
628
|
+
|
|
629
|
+
def _detect_arm_functions(self, firmware_data: bytes, base_address: int) -> list[Function]:
|
|
630
|
+
"""Detect ARM (32-bit) function boundaries.
|
|
631
|
+
|
|
632
|
+
Args:
|
|
633
|
+
firmware_data: Raw firmware binary
|
|
634
|
+
base_address: Base address for firmware
|
|
635
|
+
|
|
636
|
+
Returns:
|
|
637
|
+
List of detected functions
|
|
638
|
+
"""
|
|
639
|
+
functions: list[Function] = []
|
|
640
|
+
|
|
641
|
+
# ARM BX lr return pattern
|
|
642
|
+
bx_lr = b"\x1e\xff\x2f\xe1"
|
|
643
|
+
offset = 0
|
|
644
|
+
while True:
|
|
645
|
+
idx = firmware_data.find(bx_lr, offset)
|
|
646
|
+
if idx == -1:
|
|
647
|
+
break
|
|
648
|
+
if idx % 4 == 0: # ARM requires 4-byte alignment
|
|
649
|
+
# Assume function starts ~32 bytes before return
|
|
650
|
+
func_start = max(0, idx - 32)
|
|
651
|
+
func = Function(
|
|
652
|
+
address=base_address + func_start,
|
|
653
|
+
size=idx - func_start + 4,
|
|
654
|
+
confidence=0.5,
|
|
655
|
+
architecture=Architecture.ARM_ARM,
|
|
656
|
+
)
|
|
657
|
+
functions.append(func)
|
|
658
|
+
offset = idx + 1
|
|
659
|
+
|
|
660
|
+
return functions
|
|
661
|
+
|
|
662
|
+
def _detect_string_tables(self, firmware_data: bytes, base_address: int) -> list[StringTable]:
|
|
663
|
+
"""Detect string tables and string regions.
|
|
664
|
+
|
|
665
|
+
Args:
|
|
666
|
+
firmware_data: Raw firmware binary
|
|
667
|
+
base_address: Base address for firmware
|
|
668
|
+
|
|
669
|
+
Returns:
|
|
670
|
+
List of detected string tables
|
|
671
|
+
"""
|
|
672
|
+
string_tables: list[StringTable] = []
|
|
673
|
+
strings: list[str] = []
|
|
674
|
+
current_start = -1
|
|
675
|
+
current_string = bytearray()
|
|
676
|
+
|
|
677
|
+
for i, byte in enumerate(firmware_data):
|
|
678
|
+
if 0x20 <= byte <= 0x7E: # Printable ASCII
|
|
679
|
+
if current_start == -1:
|
|
680
|
+
current_start = i
|
|
681
|
+
current_string.append(byte)
|
|
682
|
+
elif byte == 0: # Null terminator
|
|
683
|
+
if len(current_string) >= 4: # Minimum string length
|
|
684
|
+
try:
|
|
685
|
+
decoded = current_string.decode("utf-8")
|
|
686
|
+
strings.append(decoded)
|
|
687
|
+
except UnicodeDecodeError:
|
|
688
|
+
pass
|
|
689
|
+
current_string.clear()
|
|
690
|
+
else:
|
|
691
|
+
# Non-string data
|
|
692
|
+
if len(strings) >= 3: # Minimum strings per table
|
|
693
|
+
string_tables.append(
|
|
694
|
+
StringTable(
|
|
695
|
+
address=base_address + current_start,
|
|
696
|
+
size=i - current_start,
|
|
697
|
+
strings=strings.copy(),
|
|
698
|
+
encoding="utf-8",
|
|
699
|
+
)
|
|
700
|
+
)
|
|
701
|
+
strings.clear()
|
|
702
|
+
current_start = -1
|
|
703
|
+
current_string.clear()
|
|
704
|
+
|
|
705
|
+
# Final table
|
|
706
|
+
if len(strings) >= 3:
|
|
707
|
+
string_tables.append(
|
|
708
|
+
StringTable(
|
|
709
|
+
address=base_address + current_start,
|
|
710
|
+
size=len(firmware_data) - current_start,
|
|
711
|
+
strings=strings,
|
|
712
|
+
encoding="utf-8",
|
|
713
|
+
)
|
|
714
|
+
)
|
|
715
|
+
|
|
716
|
+
return string_tables
|
|
717
|
+
|
|
718
|
+
def _detect_interrupt_vectors(
|
|
719
|
+
self, firmware_data: bytes, base_address: int, architecture: Architecture
|
|
720
|
+
) -> list[InterruptVector]:
|
|
721
|
+
"""Detect interrupt vector table.
|
|
722
|
+
|
|
723
|
+
Args:
|
|
724
|
+
firmware_data: Raw firmware binary
|
|
725
|
+
base_address: Base address for firmware
|
|
726
|
+
architecture: Detected architecture
|
|
727
|
+
|
|
728
|
+
Returns:
|
|
729
|
+
List of detected interrupt vectors
|
|
730
|
+
"""
|
|
731
|
+
vectors: list[InterruptVector] = []
|
|
732
|
+
|
|
733
|
+
# ARM Cortex-M vector table is at offset 0
|
|
734
|
+
if architecture in (Architecture.ARM_THUMB, Architecture.ARM_ARM):
|
|
735
|
+
if len(firmware_data) >= 64: # Minimum vector table size
|
|
736
|
+
try:
|
|
737
|
+
# First word is initial stack pointer
|
|
738
|
+
stack_ptr = struct.unpack("<I", firmware_data[0:4])[0]
|
|
739
|
+
vectors.append(
|
|
740
|
+
InterruptVector(index=0, address=stack_ptr, name="Initial_Stack_Pointer")
|
|
741
|
+
)
|
|
742
|
+
|
|
743
|
+
# Next words are exception/interrupt handlers
|
|
744
|
+
for i in range(1, min(16, len(firmware_data) // 4)):
|
|
745
|
+
handler_addr = struct.unpack("<I", firmware_data[i * 4 : i * 4 + 4])[0]
|
|
746
|
+
# Validate address (should be in firmware range, Thumb bit set)
|
|
747
|
+
if handler_addr != 0 and handler_addr != 0xFFFFFFFF:
|
|
748
|
+
name = self.CORTEX_M_VECTORS.get(i)
|
|
749
|
+
vectors.append(
|
|
750
|
+
InterruptVector(index=i, address=handler_addr & ~1, name=name)
|
|
751
|
+
)
|
|
752
|
+
except struct.error:
|
|
753
|
+
pass
|
|
754
|
+
|
|
755
|
+
return vectors
|
|
756
|
+
|
|
757
|
+
def _detect_compiler(self, firmware_data: bytes) -> CompilerSignature:
|
|
758
|
+
"""Detect compiler toolchain from binary signatures.
|
|
759
|
+
|
|
760
|
+
Args:
|
|
761
|
+
firmware_data: Raw firmware binary
|
|
762
|
+
|
|
763
|
+
Returns:
|
|
764
|
+
Detected compiler signature
|
|
765
|
+
"""
|
|
766
|
+
# Check for compiler-specific strings
|
|
767
|
+
if b"GCC:" in firmware_data or b"gcc version" in firmware_data:
|
|
768
|
+
return CompilerSignature.GCC
|
|
769
|
+
if b"IAR ANSI C" in firmware_data or b"ICCARM" in firmware_data:
|
|
770
|
+
return CompilerSignature.IAR
|
|
771
|
+
if b"Keil" in firmware_data or b"ARMCC" in firmware_data:
|
|
772
|
+
return CompilerSignature.KEIL
|
|
773
|
+
if b"clang version" in firmware_data or b"LLVM" in firmware_data:
|
|
774
|
+
return CompilerSignature.LLVM
|
|
775
|
+
if b"Microsoft" in firmware_data and b"Visual C++" in firmware_data:
|
|
776
|
+
return CompilerSignature.MSVC
|
|
777
|
+
|
|
778
|
+
return CompilerSignature.UNKNOWN
|
|
779
|
+
|
|
780
|
+
def _classify_regions(
|
|
781
|
+
self, firmware_data: bytes, base_address: int, architecture: Architecture
|
|
782
|
+
) -> tuple[list[tuple[int, int]], list[tuple[int, int]]]:
|
|
783
|
+
"""Classify memory regions as code or data using entropy analysis.
|
|
784
|
+
|
|
785
|
+
Args:
|
|
786
|
+
firmware_data: Raw firmware binary
|
|
787
|
+
base_address: Base address for firmware
|
|
788
|
+
architecture: Detected architecture
|
|
789
|
+
|
|
790
|
+
Returns:
|
|
791
|
+
Tuple of (code_regions, data_regions) as (address, size) pairs
|
|
792
|
+
"""
|
|
793
|
+
code_regions: list[tuple[int, int]] = []
|
|
794
|
+
data_regions: list[tuple[int, int]] = []
|
|
795
|
+
|
|
796
|
+
if len(firmware_data) < 64:
|
|
797
|
+
return code_regions, data_regions
|
|
798
|
+
|
|
799
|
+
# Analyze in 64-byte chunks
|
|
800
|
+
chunk_size = 64
|
|
801
|
+
entropies: list[float] = []
|
|
802
|
+
|
|
803
|
+
for i in range(0, len(firmware_data), chunk_size):
|
|
804
|
+
chunk = firmware_data[i : i + chunk_size]
|
|
805
|
+
if len(chunk) < chunk_size:
|
|
806
|
+
break
|
|
807
|
+
|
|
808
|
+
# Calculate Shannon entropy
|
|
809
|
+
entropy = self._calculate_entropy(chunk)
|
|
810
|
+
entropies.append(entropy)
|
|
811
|
+
|
|
812
|
+
if not entropies:
|
|
813
|
+
return code_regions, data_regions
|
|
814
|
+
|
|
815
|
+
# Code typically has medium entropy (4-7 bits/byte)
|
|
816
|
+
# Data/constants have low entropy (<4)
|
|
817
|
+
# Crypto/compressed have high entropy (>7)
|
|
818
|
+
|
|
819
|
+
current_type: str | None = None
|
|
820
|
+
region_start = 0
|
|
821
|
+
|
|
822
|
+
for i, entropy in enumerate(entropies):
|
|
823
|
+
addr = i * chunk_size
|
|
824
|
+
|
|
825
|
+
if 4.0 <= entropy <= 7.0:
|
|
826
|
+
region_type = "code"
|
|
827
|
+
else:
|
|
828
|
+
region_type = "data"
|
|
829
|
+
|
|
830
|
+
if current_type is None:
|
|
831
|
+
current_type = region_type
|
|
832
|
+
region_start = addr
|
|
833
|
+
elif current_type != region_type:
|
|
834
|
+
# Region boundary
|
|
835
|
+
size = addr - region_start
|
|
836
|
+
if current_type == "code":
|
|
837
|
+
code_regions.append((base_address + region_start, size))
|
|
838
|
+
else:
|
|
839
|
+
data_regions.append((base_address + region_start, size))
|
|
840
|
+
current_type = region_type
|
|
841
|
+
region_start = addr
|
|
842
|
+
|
|
843
|
+
# Final region
|
|
844
|
+
if current_type:
|
|
845
|
+
size = len(firmware_data) - region_start
|
|
846
|
+
if current_type == "code":
|
|
847
|
+
code_regions.append((base_address + region_start, size))
|
|
848
|
+
else:
|
|
849
|
+
data_regions.append((base_address + region_start, size))
|
|
850
|
+
|
|
851
|
+
return code_regions, data_regions
|
|
852
|
+
|
|
853
|
+
def _calculate_entropy(self, data: bytes) -> float:
|
|
854
|
+
"""Calculate Shannon entropy of byte sequence.
|
|
855
|
+
|
|
856
|
+
Args:
|
|
857
|
+
data: Byte sequence
|
|
858
|
+
|
|
859
|
+
Returns:
|
|
860
|
+
Entropy in bits per byte (0.0-8.0)
|
|
861
|
+
"""
|
|
862
|
+
if not data:
|
|
863
|
+
return 0.0
|
|
864
|
+
|
|
865
|
+
# Count byte frequencies
|
|
866
|
+
byte_counts = np.bincount(np.frombuffer(data, dtype=np.uint8), minlength=256)
|
|
867
|
+
probabilities = byte_counts / len(data)
|
|
868
|
+
|
|
869
|
+
# Calculate Shannon entropy
|
|
870
|
+
# Avoid log(0) by filtering zero probabilities
|
|
871
|
+
nonzero_probs = probabilities[probabilities > 0]
|
|
872
|
+
entropy = -np.sum(nonzero_probs * np.log2(nonzero_probs))
|
|
873
|
+
|
|
874
|
+
return float(entropy)
|