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,615 @@
|
|
|
1
|
+
"""Zigbee protocol analyzer with network topology discovery.
|
|
2
|
+
|
|
3
|
+
This module provides comprehensive Zigbee protocol analysis including
|
|
4
|
+
NWK layer parsing, APS layer decoding, ZCL cluster support, and
|
|
5
|
+
network topology discovery.
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
>>> from oscura.iot.zigbee import ZigbeeAnalyzer, ZigbeeFrame
|
|
9
|
+
>>> analyzer = ZigbeeAnalyzer()
|
|
10
|
+
>>> frame = ZigbeeFrame(
|
|
11
|
+
... timestamp=0.0,
|
|
12
|
+
... frame_type="DATA",
|
|
13
|
+
... source_address=0x1234,
|
|
14
|
+
... dest_address=0x0000,
|
|
15
|
+
... )
|
|
16
|
+
>>> analyzer.add_frame(frame)
|
|
17
|
+
>>> topology = analyzer.discover_topology()
|
|
18
|
+
|
|
19
|
+
References:
|
|
20
|
+
Zigbee Specification (CSA-IOT)
|
|
21
|
+
Zigbee NWK Layer Specification
|
|
22
|
+
Zigbee APS Layer Specification
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import json
|
|
28
|
+
from dataclasses import dataclass, field
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
from typing import Any, ClassVar
|
|
31
|
+
|
|
32
|
+
from oscura.iot.zigbee.security import parse_security_header
|
|
33
|
+
from oscura.iot.zigbee.zcl import ZCL_CLUSTERS, parse_zcl_frame
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class ZigbeeFrame:
|
|
38
|
+
"""Zigbee frame representation.
|
|
39
|
+
|
|
40
|
+
Attributes:
|
|
41
|
+
timestamp: Frame timestamp in seconds.
|
|
42
|
+
frame_type: Frame type ("DATA", "COMMAND", "ACK").
|
|
43
|
+
source_address: 16-bit network address of source.
|
|
44
|
+
dest_address: 16-bit network address of destination.
|
|
45
|
+
source_ieee: 64-bit IEEE address of source (optional).
|
|
46
|
+
dest_ieee: 64-bit IEEE address of destination (optional).
|
|
47
|
+
sequence_number: NWK sequence number.
|
|
48
|
+
radius: Maximum hop count.
|
|
49
|
+
payload: Frame payload bytes.
|
|
50
|
+
decoded_aps: Decoded APS layer data (optional).
|
|
51
|
+
decoded_zcl: Decoded ZCL data (optional).
|
|
52
|
+
|
|
53
|
+
Example:
|
|
54
|
+
>>> frame = ZigbeeFrame(
|
|
55
|
+
... timestamp=1.0,
|
|
56
|
+
... frame_type="DATA",
|
|
57
|
+
... source_address=0x1234,
|
|
58
|
+
... dest_address=0x0000,
|
|
59
|
+
... payload=b"\\x00\\x01\\x02",
|
|
60
|
+
... )
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
timestamp: float
|
|
64
|
+
frame_type: str
|
|
65
|
+
source_address: int
|
|
66
|
+
dest_address: int
|
|
67
|
+
source_ieee: int | None = None
|
|
68
|
+
dest_ieee: int | None = None
|
|
69
|
+
sequence_number: int = 0
|
|
70
|
+
radius: int = 0
|
|
71
|
+
payload: bytes = b""
|
|
72
|
+
decoded_aps: dict[str, Any] | None = None
|
|
73
|
+
decoded_zcl: dict[str, Any] | None = None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass
|
|
77
|
+
class ZigbeeDevice:
|
|
78
|
+
"""Zigbee device in network.
|
|
79
|
+
|
|
80
|
+
Attributes:
|
|
81
|
+
short_address: 16-bit network address.
|
|
82
|
+
ieee_address: 64-bit IEEE MAC address (optional).
|
|
83
|
+
device_type: Device type ("coordinator", "router", "end_device").
|
|
84
|
+
parent_address: Parent device address for routing (optional).
|
|
85
|
+
clusters: List of supported cluster IDs.
|
|
86
|
+
manufacturer: Manufacturer name (optional).
|
|
87
|
+
model: Model identifier (optional).
|
|
88
|
+
|
|
89
|
+
Example:
|
|
90
|
+
>>> device = ZigbeeDevice(
|
|
91
|
+
... short_address=0x1234,
|
|
92
|
+
... ieee_address=0x0013A20040A12345,
|
|
93
|
+
... device_type="end_device",
|
|
94
|
+
... clusters=[0x0006, 0x0008],
|
|
95
|
+
... )
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
short_address: int
|
|
99
|
+
ieee_address: int | None = None
|
|
100
|
+
device_type: str = "unknown"
|
|
101
|
+
parent_address: int | None = None
|
|
102
|
+
clusters: list[int] = field(default_factory=list)
|
|
103
|
+
manufacturer: str | None = None
|
|
104
|
+
model: str | None = None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class ZigbeeAnalyzer:
|
|
108
|
+
"""Zigbee protocol analyzer with ZCL cluster support.
|
|
109
|
+
|
|
110
|
+
Analyzes Zigbee network traffic including NWK/APS/ZCL layers,
|
|
111
|
+
discovers network topology, and supports security frame parsing.
|
|
112
|
+
|
|
113
|
+
Attributes:
|
|
114
|
+
STANDARD_CLUSTERS: Standard ZCL cluster ID to name mapping.
|
|
115
|
+
FRAME_TYPES: NWK frame type codes.
|
|
116
|
+
|
|
117
|
+
Example:
|
|
118
|
+
>>> analyzer = ZigbeeAnalyzer()
|
|
119
|
+
>>> analyzer.add_frame(frame)
|
|
120
|
+
>>> topology = analyzer.discover_topology()
|
|
121
|
+
>>> analyzer.export_topology(Path("network.json"))
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
# Standard ZCL cluster IDs
|
|
125
|
+
STANDARD_CLUSTERS: ClassVar[dict[int, str]] = ZCL_CLUSTERS
|
|
126
|
+
|
|
127
|
+
# Frame types
|
|
128
|
+
FRAME_TYPES: ClassVar[dict[int, str]] = {
|
|
129
|
+
0x00: "DATA",
|
|
130
|
+
0x01: "COMMAND",
|
|
131
|
+
0x02: "ACK",
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
def __init__(self) -> None:
|
|
135
|
+
"""Initialize Zigbee analyzer.
|
|
136
|
+
|
|
137
|
+
Example:
|
|
138
|
+
>>> analyzer = ZigbeeAnalyzer()
|
|
139
|
+
>>> len(analyzer.frames)
|
|
140
|
+
0
|
|
141
|
+
"""
|
|
142
|
+
self.frames: list[ZigbeeFrame] = []
|
|
143
|
+
self.devices: dict[int, ZigbeeDevice] = {}
|
|
144
|
+
self.network_keys: list[bytes] = []
|
|
145
|
+
|
|
146
|
+
def add_frame(self, frame: ZigbeeFrame) -> None:
|
|
147
|
+
"""Add Zigbee frame for analysis.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
frame: Zigbee frame to analyze.
|
|
151
|
+
|
|
152
|
+
Example:
|
|
153
|
+
>>> analyzer = ZigbeeAnalyzer()
|
|
154
|
+
>>> frame = ZigbeeFrame(
|
|
155
|
+
... timestamp=0.0,
|
|
156
|
+
... frame_type="DATA",
|
|
157
|
+
... source_address=0x1234,
|
|
158
|
+
... dest_address=0x0000,
|
|
159
|
+
... )
|
|
160
|
+
>>> analyzer.add_frame(frame)
|
|
161
|
+
>>> len(analyzer.frames)
|
|
162
|
+
1
|
|
163
|
+
"""
|
|
164
|
+
self.frames.append(frame)
|
|
165
|
+
|
|
166
|
+
# Update device registry
|
|
167
|
+
if frame.source_address not in self.devices:
|
|
168
|
+
self.devices[frame.source_address] = ZigbeeDevice(
|
|
169
|
+
short_address=frame.source_address,
|
|
170
|
+
ieee_address=frame.source_ieee,
|
|
171
|
+
)
|
|
172
|
+
elif frame.source_ieee and not self.devices[frame.source_address].ieee_address:
|
|
173
|
+
self.devices[frame.source_address].ieee_address = frame.source_ieee
|
|
174
|
+
|
|
175
|
+
if frame.dest_address not in self.devices and frame.dest_address < 0xFFF0:
|
|
176
|
+
# Don't add broadcast addresses
|
|
177
|
+
self.devices[frame.dest_address] = ZigbeeDevice(
|
|
178
|
+
short_address=frame.dest_address,
|
|
179
|
+
ieee_address=frame.dest_ieee,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
def parse_nwk_layer(self, data: bytes) -> dict[str, Any]:
|
|
183
|
+
"""Parse Zigbee network layer frame.
|
|
184
|
+
|
|
185
|
+
Parses NWK frame structure including frame control, addresses,
|
|
186
|
+
sequence number, radius, and optional IEEE addresses.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
data: Raw NWK layer frame bytes.
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
Parsed NWK layer fields including addresses and payload.
|
|
193
|
+
|
|
194
|
+
Raises:
|
|
195
|
+
ValueError: If data is too short for NWK header.
|
|
196
|
+
|
|
197
|
+
Example:
|
|
198
|
+
>>> analyzer = ZigbeeAnalyzer()
|
|
199
|
+
>>> nwk_data = bytes([0x08, 0x00, 0x00, 0x00, 0x34, 0x12, 0x1E, 0x01])
|
|
200
|
+
>>> result = analyzer.parse_nwk_layer(nwk_data)
|
|
201
|
+
>>> result['source_address']
|
|
202
|
+
4660
|
|
203
|
+
"""
|
|
204
|
+
if len(data) < 8:
|
|
205
|
+
raise ValueError("Insufficient data for NWK header")
|
|
206
|
+
|
|
207
|
+
# Parse frame control and basic fields
|
|
208
|
+
frame_ctrl_data = self._parse_nwk_frame_control(data)
|
|
209
|
+
|
|
210
|
+
# Parse optional fields
|
|
211
|
+
offset, dest_ieee, src_ieee, security_data = self._parse_nwk_optional_fields(
|
|
212
|
+
data, frame_ctrl_data, offset=8
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
# Extract payload
|
|
216
|
+
payload = data[offset:]
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
**frame_ctrl_data,
|
|
220
|
+
"dest_ieee": dest_ieee,
|
|
221
|
+
"source_ieee": src_ieee,
|
|
222
|
+
"security_data": security_data,
|
|
223
|
+
"payload": payload,
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
def _parse_nwk_frame_control(self, data: bytes) -> dict[str, Any]:
|
|
227
|
+
"""Parse NWK frame control field and basic header.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
data: Raw NWK frame bytes.
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
Dictionary of frame control flags and basic fields.
|
|
234
|
+
"""
|
|
235
|
+
frame_control = int.from_bytes(data[0:2], "little")
|
|
236
|
+
frame_type = (frame_control >> 0) & 0x03
|
|
237
|
+
protocol_version = (frame_control >> 2) & 0x0F
|
|
238
|
+
discover_route = (frame_control >> 6) & 0x03
|
|
239
|
+
multicast_flag = bool((frame_control >> 8) & 0x01)
|
|
240
|
+
security = bool((frame_control >> 9) & 0x01)
|
|
241
|
+
source_route = bool((frame_control >> 10) & 0x01)
|
|
242
|
+
dest_ieee_present = bool((frame_control >> 11) & 0x01)
|
|
243
|
+
src_ieee_present = bool((frame_control >> 12) & 0x01)
|
|
244
|
+
|
|
245
|
+
dest_addr = int.from_bytes(data[2:4], "little")
|
|
246
|
+
src_addr = int.from_bytes(data[4:6], "little")
|
|
247
|
+
radius = data[6]
|
|
248
|
+
sequence = data[7]
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
"frame_type": self.FRAME_TYPES.get(frame_type, "UNKNOWN"),
|
|
252
|
+
"protocol_version": protocol_version,
|
|
253
|
+
"discover_route": discover_route,
|
|
254
|
+
"multicast": multicast_flag,
|
|
255
|
+
"security": security,
|
|
256
|
+
"source_route": source_route,
|
|
257
|
+
"dest_ieee_present": dest_ieee_present,
|
|
258
|
+
"src_ieee_present": src_ieee_present,
|
|
259
|
+
"dest_address": dest_addr,
|
|
260
|
+
"source_address": src_addr,
|
|
261
|
+
"radius": radius,
|
|
262
|
+
"sequence": sequence,
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
def _parse_nwk_optional_fields(
|
|
266
|
+
self, data: bytes, frame_ctrl: dict[str, Any], offset: int
|
|
267
|
+
) -> tuple[int, int | None, int | None, dict[str, Any] | None]:
|
|
268
|
+
"""Parse optional NWK fields (IEEE addresses, multicast, source route, security).
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
data: Raw NWK frame bytes.
|
|
272
|
+
frame_ctrl: Parsed frame control data.
|
|
273
|
+
offset: Current offset in data.
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
Tuple of (new_offset, dest_ieee, src_ieee, security_data).
|
|
277
|
+
|
|
278
|
+
Raises:
|
|
279
|
+
ValueError: If data is insufficient for optional fields.
|
|
280
|
+
"""
|
|
281
|
+
# Optional destination IEEE address
|
|
282
|
+
dest_ieee = None
|
|
283
|
+
if frame_ctrl["dest_ieee_present"]:
|
|
284
|
+
if len(data) < offset + 8:
|
|
285
|
+
raise ValueError("Insufficient data for destination IEEE address")
|
|
286
|
+
dest_ieee = int.from_bytes(data[offset : offset + 8], "little")
|
|
287
|
+
offset += 8
|
|
288
|
+
|
|
289
|
+
# Optional source IEEE address
|
|
290
|
+
src_ieee = None
|
|
291
|
+
if frame_ctrl["src_ieee_present"]:
|
|
292
|
+
if len(data) < offset + 8:
|
|
293
|
+
raise ValueError("Insufficient data for source IEEE address")
|
|
294
|
+
src_ieee = int.from_bytes(data[offset : offset + 8], "little")
|
|
295
|
+
offset += 8
|
|
296
|
+
|
|
297
|
+
# Multicast control
|
|
298
|
+
if frame_ctrl["multicast"]:
|
|
299
|
+
if len(data) < offset + 1:
|
|
300
|
+
raise ValueError("Insufficient data for multicast control")
|
|
301
|
+
offset += 1
|
|
302
|
+
|
|
303
|
+
# Source route subframe
|
|
304
|
+
if frame_ctrl["source_route"]:
|
|
305
|
+
if len(data) < offset + 1:
|
|
306
|
+
raise ValueError("Insufficient data for source route")
|
|
307
|
+
relay_count = data[offset]
|
|
308
|
+
offset += 2 + (relay_count * 2)
|
|
309
|
+
|
|
310
|
+
# Security header
|
|
311
|
+
security_data = None
|
|
312
|
+
if frame_ctrl["security"]:
|
|
313
|
+
if len(data) < offset + 5:
|
|
314
|
+
raise ValueError("Insufficient data for security header")
|
|
315
|
+
security_header = parse_security_header(data[offset:])
|
|
316
|
+
if "error" in security_header:
|
|
317
|
+
raise ValueError(security_header["error"])
|
|
318
|
+
security_data = security_header
|
|
319
|
+
offset += security_header["header_length"]
|
|
320
|
+
|
|
321
|
+
return offset, dest_ieee, src_ieee, security_data
|
|
322
|
+
|
|
323
|
+
def parse_aps_layer(self, data: bytes) -> dict[str, Any]:
|
|
324
|
+
"""Parse Zigbee APS (Application Support) layer.
|
|
325
|
+
|
|
326
|
+
Parses APS frame including frame control, addressing, and cluster ID.
|
|
327
|
+
|
|
328
|
+
Args:
|
|
329
|
+
data: APS layer frame bytes.
|
|
330
|
+
|
|
331
|
+
Returns:
|
|
332
|
+
Parsed APS layer fields.
|
|
333
|
+
|
|
334
|
+
Raises:
|
|
335
|
+
ValueError: If data is too short for APS header.
|
|
336
|
+
|
|
337
|
+
Example:
|
|
338
|
+
>>> analyzer = ZigbeeAnalyzer()
|
|
339
|
+
>>> aps_data = bytes([0x00, 0x00, 0x06, 0x00, 0x01])
|
|
340
|
+
>>> result = analyzer.parse_aps_layer(aps_data)
|
|
341
|
+
>>> result['cluster_id']
|
|
342
|
+
6
|
|
343
|
+
"""
|
|
344
|
+
if len(data) < 3:
|
|
345
|
+
raise ValueError("Insufficient data for APS header")
|
|
346
|
+
|
|
347
|
+
# Parse frame control
|
|
348
|
+
frame_ctrl = self._parse_aps_frame_control(data[0])
|
|
349
|
+
|
|
350
|
+
# Parse addressing and IDs
|
|
351
|
+
offset, addressing_data = self._parse_aps_addressing(data, frame_ctrl, offset=1)
|
|
352
|
+
|
|
353
|
+
# Parse extended header if present
|
|
354
|
+
if frame_ctrl["extended_header"]:
|
|
355
|
+
if len(data) >= offset + 1:
|
|
356
|
+
offset += 1 # Skip extended frame control
|
|
357
|
+
|
|
358
|
+
payload = data[offset:]
|
|
359
|
+
|
|
360
|
+
cluster_id = addressing_data.get("cluster_id")
|
|
361
|
+
cluster_name = (
|
|
362
|
+
self.STANDARD_CLUSTERS.get(cluster_id, "Unknown")
|
|
363
|
+
if cluster_id is not None and isinstance(cluster_id, int)
|
|
364
|
+
else None
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
return {
|
|
368
|
+
**frame_ctrl,
|
|
369
|
+
**addressing_data,
|
|
370
|
+
"cluster_name": cluster_name,
|
|
371
|
+
"payload": payload,
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
def _parse_aps_frame_control(self, frame_control: int) -> dict[str, Any]:
|
|
375
|
+
"""Parse APS frame control byte.
|
|
376
|
+
|
|
377
|
+
Args:
|
|
378
|
+
frame_control: Frame control byte value.
|
|
379
|
+
|
|
380
|
+
Returns:
|
|
381
|
+
Dictionary of frame control flags.
|
|
382
|
+
"""
|
|
383
|
+
return {
|
|
384
|
+
"frame_type": frame_control & 0x03,
|
|
385
|
+
"delivery_mode": (frame_control >> 2) & 0x03,
|
|
386
|
+
"security": bool((frame_control >> 5) & 0x01),
|
|
387
|
+
"ack_request": bool((frame_control >> 6) & 0x01),
|
|
388
|
+
"extended_header": bool((frame_control >> 7) & 0x01),
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
def _parse_aps_addressing(
|
|
392
|
+
self, data: bytes, frame_ctrl: dict[str, Any], offset: int
|
|
393
|
+
) -> tuple[int, dict[str, Any]]:
|
|
394
|
+
"""Parse APS addressing fields (endpoints, cluster, profile).
|
|
395
|
+
|
|
396
|
+
Args:
|
|
397
|
+
data: APS frame bytes.
|
|
398
|
+
frame_ctrl: Parsed frame control data.
|
|
399
|
+
offset: Current offset in data.
|
|
400
|
+
|
|
401
|
+
Returns:
|
|
402
|
+
Tuple of (new_offset, addressing_data).
|
|
403
|
+
|
|
404
|
+
Raises:
|
|
405
|
+
ValueError: If data is insufficient for addressing fields.
|
|
406
|
+
"""
|
|
407
|
+
addressing: dict[str, Any] = {
|
|
408
|
+
"dest_endpoint": None,
|
|
409
|
+
"group_address": None,
|
|
410
|
+
"cluster_id": None,
|
|
411
|
+
"profile_id": None,
|
|
412
|
+
"source_endpoint": None,
|
|
413
|
+
"aps_counter": None,
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
delivery_mode = frame_ctrl["delivery_mode"]
|
|
417
|
+
|
|
418
|
+
# Destination endpoint (for unicast/broadcast)
|
|
419
|
+
if delivery_mode in [0x00, 0x01, 0x02]:
|
|
420
|
+
if len(data) < offset + 1:
|
|
421
|
+
raise ValueError("Insufficient data for destination endpoint")
|
|
422
|
+
addressing["dest_endpoint"] = data[offset]
|
|
423
|
+
offset += 1
|
|
424
|
+
|
|
425
|
+
# Group address (for group delivery)
|
|
426
|
+
if delivery_mode == 0x01:
|
|
427
|
+
if len(data) < offset + 2:
|
|
428
|
+
raise ValueError("Insufficient data for group address")
|
|
429
|
+
addressing["group_address"] = int.from_bytes(data[offset : offset + 2], "little")
|
|
430
|
+
offset += 2
|
|
431
|
+
|
|
432
|
+
# Cluster ID
|
|
433
|
+
if len(data) >= offset + 2:
|
|
434
|
+
addressing["cluster_id"] = int.from_bytes(data[offset : offset + 2], "little")
|
|
435
|
+
offset += 2
|
|
436
|
+
|
|
437
|
+
# Profile ID
|
|
438
|
+
if len(data) >= offset + 2:
|
|
439
|
+
addressing["profile_id"] = int.from_bytes(data[offset : offset + 2], "little")
|
|
440
|
+
offset += 2
|
|
441
|
+
|
|
442
|
+
# Source endpoint
|
|
443
|
+
if len(data) >= offset + 1:
|
|
444
|
+
addressing["source_endpoint"] = data[offset]
|
|
445
|
+
offset += 1
|
|
446
|
+
|
|
447
|
+
# APS counter
|
|
448
|
+
if len(data) >= offset + 1:
|
|
449
|
+
addressing["aps_counter"] = data[offset]
|
|
450
|
+
offset += 1
|
|
451
|
+
|
|
452
|
+
return offset, addressing
|
|
453
|
+
|
|
454
|
+
def parse_zcl_frame(self, cluster_id: int, data: bytes) -> dict[str, Any]:
|
|
455
|
+
"""Parse ZCL frame for specific cluster.
|
|
456
|
+
|
|
457
|
+
Wrapper around zcl.parse_zcl_frame for integration with analyzer.
|
|
458
|
+
|
|
459
|
+
Args:
|
|
460
|
+
cluster_id: ZCL cluster ID.
|
|
461
|
+
data: ZCL frame payload.
|
|
462
|
+
|
|
463
|
+
Returns:
|
|
464
|
+
Parsed ZCL frame data.
|
|
465
|
+
|
|
466
|
+
Example:
|
|
467
|
+
>>> analyzer = ZigbeeAnalyzer()
|
|
468
|
+
>>> zcl_data = bytes([0x01, 0x00, 0x01])
|
|
469
|
+
>>> result = analyzer.parse_zcl_frame(0x0006, zcl_data)
|
|
470
|
+
>>> result['command_name']
|
|
471
|
+
'On'
|
|
472
|
+
"""
|
|
473
|
+
return parse_zcl_frame(cluster_id, data)
|
|
474
|
+
|
|
475
|
+
def discover_topology(self) -> dict[int, list[int]]:
|
|
476
|
+
"""Discover network topology from captured frames.
|
|
477
|
+
|
|
478
|
+
Analyzes frame routing to determine parent-child relationships
|
|
479
|
+
in the Zigbee network tree.
|
|
480
|
+
|
|
481
|
+
Returns:
|
|
482
|
+
Dictionary mapping parent addresses to list of child addresses.
|
|
483
|
+
|
|
484
|
+
Example:
|
|
485
|
+
>>> analyzer = ZigbeeAnalyzer()
|
|
486
|
+
>>> # Add frames...
|
|
487
|
+
>>> topology = analyzer.discover_topology()
|
|
488
|
+
>>> print(topology[0x0000]) # Coordinator's children
|
|
489
|
+
[4660, 8765]
|
|
490
|
+
"""
|
|
491
|
+
topology: dict[int, list[int]] = {}
|
|
492
|
+
|
|
493
|
+
# Analyze frame patterns to infer topology
|
|
494
|
+
# Devices that communicate frequently with coordinator are likely direct children
|
|
495
|
+
# Devices that route through others are likely children of routers
|
|
496
|
+
|
|
497
|
+
for frame in self.frames:
|
|
498
|
+
# If a device sends to coordinator (0x0000), it might be a direct child
|
|
499
|
+
if frame.dest_address == 0x0000 and frame.source_address not in [0x0000]:
|
|
500
|
+
if 0x0000 not in topology:
|
|
501
|
+
topology[0x0000] = []
|
|
502
|
+
if frame.source_address not in topology[0x0000]:
|
|
503
|
+
topology[0x0000].append(frame.source_address)
|
|
504
|
+
|
|
505
|
+
# Infer device types from topology
|
|
506
|
+
for addr, device in self.devices.items():
|
|
507
|
+
if addr == 0x0000:
|
|
508
|
+
device.device_type = "coordinator"
|
|
509
|
+
elif addr in topology:
|
|
510
|
+
device.device_type = "router" # Has children
|
|
511
|
+
else:
|
|
512
|
+
device.device_type = "end_device" # No children
|
|
513
|
+
|
|
514
|
+
# Set parent address if known
|
|
515
|
+
for parent, children in topology.items():
|
|
516
|
+
if addr in children:
|
|
517
|
+
device.parent_address = parent
|
|
518
|
+
|
|
519
|
+
return topology
|
|
520
|
+
|
|
521
|
+
def add_network_key(self, key: bytes) -> None:
|
|
522
|
+
"""Add network key for decrypting secured frames.
|
|
523
|
+
|
|
524
|
+
Args:
|
|
525
|
+
key: 128-bit (16-byte) AES network key.
|
|
526
|
+
|
|
527
|
+
Raises:
|
|
528
|
+
ValueError: If key is not 16 bytes.
|
|
529
|
+
|
|
530
|
+
Example:
|
|
531
|
+
>>> analyzer = ZigbeeAnalyzer()
|
|
532
|
+
>>> key = bytes([0x01] * 16)
|
|
533
|
+
>>> analyzer.add_network_key(key)
|
|
534
|
+
>>> len(analyzer.network_keys)
|
|
535
|
+
1
|
|
536
|
+
"""
|
|
537
|
+
if len(key) != 16:
|
|
538
|
+
raise ValueError(f"Network key must be 16 bytes, got {len(key)}")
|
|
539
|
+
self.network_keys.append(key)
|
|
540
|
+
|
|
541
|
+
def export_topology(self, output_path: Path) -> None:
|
|
542
|
+
"""Export network topology as JSON with device information.
|
|
543
|
+
|
|
544
|
+
Exports topology data including devices, relationships, and
|
|
545
|
+
cluster information in JSON format. Also generates GraphViz
|
|
546
|
+
DOT format for visualization.
|
|
547
|
+
|
|
548
|
+
Args:
|
|
549
|
+
output_path: Path to output JSON file.
|
|
550
|
+
|
|
551
|
+
Example:
|
|
552
|
+
>>> analyzer = ZigbeeAnalyzer()
|
|
553
|
+
>>> # Add frames and analyze...
|
|
554
|
+
>>> analyzer.export_topology(Path("zigbee_network.json"))
|
|
555
|
+
"""
|
|
556
|
+
topology = self.discover_topology()
|
|
557
|
+
|
|
558
|
+
export_data = {
|
|
559
|
+
"devices": {
|
|
560
|
+
addr: {
|
|
561
|
+
"short_address": f"0x{addr:04X}",
|
|
562
|
+
"ieee_address": f"0x{dev.ieee_address:016X}" if dev.ieee_address else None,
|
|
563
|
+
"device_type": dev.device_type,
|
|
564
|
+
"parent_address": f"0x{dev.parent_address:04X}" if dev.parent_address else None,
|
|
565
|
+
"clusters": [
|
|
566
|
+
{
|
|
567
|
+
"id": f"0x{cid:04X}",
|
|
568
|
+
"name": self.STANDARD_CLUSTERS.get(cid, "Unknown"),
|
|
569
|
+
}
|
|
570
|
+
for cid in dev.clusters
|
|
571
|
+
],
|
|
572
|
+
"manufacturer": dev.manufacturer,
|
|
573
|
+
"model": dev.model,
|
|
574
|
+
}
|
|
575
|
+
for addr, dev in self.devices.items()
|
|
576
|
+
},
|
|
577
|
+
"topology": {
|
|
578
|
+
f"0x{parent:04X}": [f"0x{child:04X}" for child in children]
|
|
579
|
+
for parent, children in topology.items()
|
|
580
|
+
},
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
# Write JSON
|
|
584
|
+
with output_path.open("w") as f:
|
|
585
|
+
json.dump(export_data, f, indent=2)
|
|
586
|
+
|
|
587
|
+
# Generate GraphViz DOT file
|
|
588
|
+
dot_path = output_path.with_suffix(".dot")
|
|
589
|
+
with dot_path.open("w") as f:
|
|
590
|
+
f.write("digraph ZigbeeNetwork {\n")
|
|
591
|
+
f.write(" rankdir=TB;\n")
|
|
592
|
+
f.write(" node [shape=box];\n\n")
|
|
593
|
+
|
|
594
|
+
# Add nodes
|
|
595
|
+
for addr, dev in self.devices.items():
|
|
596
|
+
label = f"0x{addr:04X}\\n{dev.device_type}"
|
|
597
|
+
if dev.manufacturer:
|
|
598
|
+
label += f"\\n{dev.manufacturer}"
|
|
599
|
+
if dev.model:
|
|
600
|
+
label += f"\\n{dev.model}"
|
|
601
|
+
|
|
602
|
+
shape = "ellipse" if dev.device_type == "coordinator" else "box"
|
|
603
|
+
f.write(f' "0x{addr:04X}" [label="{label}", shape={shape}];\n')
|
|
604
|
+
|
|
605
|
+
f.write("\n")
|
|
606
|
+
|
|
607
|
+
# Add edges
|
|
608
|
+
for parent, children in topology.items():
|
|
609
|
+
for child in children:
|
|
610
|
+
f.write(f' "0x{parent:04X}" -> "0x{child:04X}";\n')
|
|
611
|
+
|
|
612
|
+
f.write("}\n")
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
__all__ = ["ZigbeeAnalyzer", "ZigbeeDevice", "ZigbeeFrame"]
|