oscura 0.5.1__py3-none-any.whl → 0.7.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/edges.py +325 -65
- 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/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 +17 -102
- 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/{schemas → core/schemas}/device_mapping.json +2 -8
- oscura/{schemas → core/schemas}/packet_format.json +4 -24
- oscura/{schemas → core/schemas}/protocol_definition.json +2 -12
- 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 -8
- oscura/export/kaitai_struct.py +513 -0
- oscura/export/scapy_layer.py +801 -0
- 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/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 +183 -67
- 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/tss.py +456 -0
- oscura/loaders/vcd.py +215 -117
- oscura/loaders/wav.py +155 -68
- oscura/reporting/__init__.py +9 -0
- 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/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/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 +1 -1
- 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.7.0.dist-info/METADATA +661 -0
- oscura-0.7.0.dist-info/RECORD +591 -0
- 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/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 -291
- 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/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/workflow/__init__.py +0 -13
- oscura-0.5.1.dist-info/METADATA +0 -583
- oscura-0.5.1.dist-info/RECORD +0 -481
- /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/{schemas → core/schemas}/bus_configuration.json +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.1.dist-info → oscura-0.7.0.dist-info}/WHEEL +0 -0
- {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/entry_points.txt +0 -0
- {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
"""OPC UA (Unified Architecture) protocol analyzer.
|
|
2
|
+
|
|
3
|
+
This module provides comprehensive OPC UA binary protocol analysis for
|
|
4
|
+
industrial automation and SCADA systems communication. Supports message
|
|
5
|
+
parsing, service decoding, security policy handling, and address space export.
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
>>> from oscura.analyzers.protocols.industrial.opcua import OPCUAAnalyzer
|
|
9
|
+
>>> analyzer = OPCUAAnalyzer()
|
|
10
|
+
>>> # Parse Hello message
|
|
11
|
+
>>> hello = bytes([0x48, 0x45, 0x4C, 0x46, 0x1C, 0x00, 0x00, 0x00, ...])
|
|
12
|
+
>>> msg = analyzer.parse_message(hello, timestamp=0.0)
|
|
13
|
+
>>> print(f"{msg.message_type}: {msg.decoded_service}")
|
|
14
|
+
>>> # Export discovered address space
|
|
15
|
+
>>> analyzer.export_address_space(Path("opcua_nodes.json"))
|
|
16
|
+
|
|
17
|
+
References:
|
|
18
|
+
OPC UA Part 6: Mappings (Binary Protocol)
|
|
19
|
+
https://reference.opcfoundation.org/Core/Part6/v105/docs/
|
|
20
|
+
|
|
21
|
+
OPC UA Part 4: Services
|
|
22
|
+
https://reference.opcfoundation.org/Core/Part4/v105/docs/
|
|
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.analyzers.protocols.industrial.opcua.datatypes import parse_string
|
|
33
|
+
from oscura.analyzers.protocols.industrial.opcua.services import SERVICE_PARSERS
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class OPCUAMessage:
|
|
38
|
+
"""OPC UA message representation.
|
|
39
|
+
|
|
40
|
+
Attributes:
|
|
41
|
+
timestamp: Message timestamp in seconds.
|
|
42
|
+
message_type: Message type ("HEL", "ACK", "OPN", "CLO", "MSG", "ERR").
|
|
43
|
+
is_final: True if final chunk ('F' flag).
|
|
44
|
+
chunk_type: Chunk type character ('F', 'C', 'A') or None.
|
|
45
|
+
secure_channel_id: Secure channel identifier.
|
|
46
|
+
security_token_id: Security token ID (for secure messages).
|
|
47
|
+
sequence_number: Message sequence number (for MSG chunks).
|
|
48
|
+
request_id: Request identifier (for MSG chunks).
|
|
49
|
+
service_id: Service type identifier (NodeId).
|
|
50
|
+
service_name: Human-readable service name.
|
|
51
|
+
payload: Raw payload bytes.
|
|
52
|
+
decoded_service: Parsed service-specific data.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
timestamp: float
|
|
56
|
+
message_type: str # "HEL", "ACK", "OPN", "CLO", "MSG", "ERR"
|
|
57
|
+
is_final: bool # 'F' flag
|
|
58
|
+
chunk_type: str | None = None # For chunked messages
|
|
59
|
+
secure_channel_id: int = 0
|
|
60
|
+
security_token_id: int | None = None
|
|
61
|
+
sequence_number: int | None = None
|
|
62
|
+
request_id: int | None = None
|
|
63
|
+
service_id: int | None = None
|
|
64
|
+
service_name: str | None = None
|
|
65
|
+
payload: bytes = b""
|
|
66
|
+
decoded_service: dict[str, Any] = field(default_factory=dict)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass
|
|
70
|
+
class OPCUANode:
|
|
71
|
+
"""OPC UA node in address space.
|
|
72
|
+
|
|
73
|
+
Represents a node in the OPC UA information model address space.
|
|
74
|
+
|
|
75
|
+
Attributes:
|
|
76
|
+
node_id: Node identifier string (e.g., "ns=2;i=1001").
|
|
77
|
+
node_class: Node class ("Object", "Variable", "Method", etc.).
|
|
78
|
+
browse_name: Qualified name for browsing.
|
|
79
|
+
display_name: Localized display name.
|
|
80
|
+
value: Current value (for Variable nodes).
|
|
81
|
+
data_type: Data type identifier (for Variable nodes).
|
|
82
|
+
children: List of child node IDs.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
node_id: str # e.g., "ns=2;i=1001"
|
|
86
|
+
node_class: str # "Object", "Variable", "Method", etc.
|
|
87
|
+
browse_name: str | None = None
|
|
88
|
+
display_name: str | None = None
|
|
89
|
+
value: Any = None
|
|
90
|
+
data_type: str | None = None
|
|
91
|
+
children: list[str] = field(default_factory=list)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class OPCUAAnalyzer:
|
|
95
|
+
"""OPC UA protocol analyzer for binary protocol.
|
|
96
|
+
|
|
97
|
+
Provides comprehensive OPC UA protocol analysis including message parsing,
|
|
98
|
+
service decoding, security handling, and address space discovery.
|
|
99
|
+
|
|
100
|
+
Attributes:
|
|
101
|
+
messages: List of parsed OPC UA messages.
|
|
102
|
+
nodes: Dictionary of discovered nodes by node ID.
|
|
103
|
+
security_mode: Current security mode ("None", "Sign", "SignAndEncrypt").
|
|
104
|
+
|
|
105
|
+
Example:
|
|
106
|
+
>>> analyzer = OPCUAAnalyzer()
|
|
107
|
+
>>> # Parse Hello message
|
|
108
|
+
>>> hello_msg = bytes([0x48, 0x45, 0x4C, 0x46, ...]) # HEL + F
|
|
109
|
+
>>> msg = analyzer.parse_message(hello_msg)
|
|
110
|
+
>>> assert msg.message_type == "HEL"
|
|
111
|
+
>>> # Parse MSG chunk
|
|
112
|
+
>>> msg_chunk = bytes([0x4D, 0x53, 0x47, 0x46, ...]) # MSG + F
|
|
113
|
+
>>> msg = analyzer.parse_message(msg_chunk)
|
|
114
|
+
>>> print(f"Service: {msg.service_name}")
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
# Message type constants (3-byte identifiers)
|
|
118
|
+
MESSAGE_TYPES: ClassVar[dict[int, str]] = {
|
|
119
|
+
0x48454C: "HEL", # Hello (H E L)
|
|
120
|
+
0x41434B: "ACK", # Acknowledge (A C K)
|
|
121
|
+
0x4F504E: "OPN", # Open Secure Channel (O P N)
|
|
122
|
+
0x434C4F: "CLO", # Close Secure Channel (C L O)
|
|
123
|
+
0x4D5347: "MSG", # Message (M S G)
|
|
124
|
+
0x455252: "ERR", # Error (E R R)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
# Service IDs (subset of most common services)
|
|
128
|
+
SERVICE_IDS: ClassVar[dict[int, str]] = {
|
|
129
|
+
421: "ReadRequest",
|
|
130
|
+
424: "ReadResponse",
|
|
131
|
+
673: "WriteRequest",
|
|
132
|
+
676: "WriteResponse",
|
|
133
|
+
527: "BrowseRequest",
|
|
134
|
+
530: "BrowseResponse",
|
|
135
|
+
631: "CreateSubscriptionRequest",
|
|
136
|
+
634: "CreateSubscriptionResponse",
|
|
137
|
+
826: "PublishRequest",
|
|
138
|
+
829: "PublishResponse",
|
|
139
|
+
445: "GetEndpointsRequest",
|
|
140
|
+
448: "GetEndpointsResponse",
|
|
141
|
+
461: "OpenSecureChannelRequest",
|
|
142
|
+
464: "OpenSecureChannelResponse",
|
|
143
|
+
465: "CloseSecureChannelRequest",
|
|
144
|
+
468: "CloseSecureChannelResponse",
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
# Node class enumeration
|
|
148
|
+
NODE_CLASSES: ClassVar[dict[int, str]] = {
|
|
149
|
+
1: "Object",
|
|
150
|
+
2: "Variable",
|
|
151
|
+
4: "Method",
|
|
152
|
+
8: "ObjectType",
|
|
153
|
+
16: "VariableType",
|
|
154
|
+
32: "ReferenceType",
|
|
155
|
+
64: "DataType",
|
|
156
|
+
128: "View",
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
def __init__(self) -> None:
|
|
160
|
+
"""Initialize OPC UA analyzer."""
|
|
161
|
+
self.messages: list[OPCUAMessage] = []
|
|
162
|
+
self.nodes: dict[str, OPCUANode] = {}
|
|
163
|
+
self.security_mode: str = "None"
|
|
164
|
+
|
|
165
|
+
def parse_message(self, data: bytes, timestamp: float = 0.0) -> OPCUAMessage:
|
|
166
|
+
"""Parse OPC UA binary protocol message.
|
|
167
|
+
|
|
168
|
+
Message Header (8 bytes):
|
|
169
|
+
- MessageType (3 bytes) - "HEL", "ACK", "OPN", "MSG", etc.
|
|
170
|
+
- ChunkType (1 byte) - 'F' (Final), 'C' (Continue), 'A' (Abort)
|
|
171
|
+
- MessageSize (4 bytes, little-endian) - Total message size
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
data: Complete message bytes including header.
|
|
175
|
+
timestamp: Message timestamp in seconds.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
Parsed OPC UA message.
|
|
179
|
+
|
|
180
|
+
Raises:
|
|
181
|
+
ValueError: If message is invalid.
|
|
182
|
+
|
|
183
|
+
Example:
|
|
184
|
+
>>> analyzer = OPCUAAnalyzer()
|
|
185
|
+
>>> # Hello message
|
|
186
|
+
>>> hello = bytes([0x48, 0x45, 0x4C, 0x46, 0x1C, 0x00, 0x00, 0x00])
|
|
187
|
+
>>> msg = analyzer.parse_message(hello)
|
|
188
|
+
>>> assert msg.message_type == "HEL"
|
|
189
|
+
>>> assert msg.is_final is True
|
|
190
|
+
"""
|
|
191
|
+
if len(data) < 8:
|
|
192
|
+
raise ValueError(f"OPC UA message too short: {len(data)} bytes (minimum 8)")
|
|
193
|
+
|
|
194
|
+
# Parse common header
|
|
195
|
+
header = self._parse_header(data)
|
|
196
|
+
|
|
197
|
+
msg_type = header["message_type"]
|
|
198
|
+
chunk_type = header["chunk_type"]
|
|
199
|
+
is_final = chunk_type == "F"
|
|
200
|
+
|
|
201
|
+
# Parse type-specific payload
|
|
202
|
+
decoded: dict[str, Any] = {}
|
|
203
|
+
payload = data[8:]
|
|
204
|
+
|
|
205
|
+
if msg_type == "HEL":
|
|
206
|
+
decoded = self._parse_hello(payload)
|
|
207
|
+
elif msg_type == "ACK":
|
|
208
|
+
decoded = self._parse_acknowledge(payload)
|
|
209
|
+
elif msg_type == "OPN":
|
|
210
|
+
decoded = self._parse_open_secure_channel(payload)
|
|
211
|
+
elif msg_type == "CLO":
|
|
212
|
+
decoded = self._parse_open_secure_channel(payload) # Same format as OPN
|
|
213
|
+
elif msg_type == "MSG":
|
|
214
|
+
decoded = self._parse_message_chunk(payload)
|
|
215
|
+
elif msg_type == "ERR":
|
|
216
|
+
decoded = self._parse_error(payload)
|
|
217
|
+
|
|
218
|
+
message = OPCUAMessage(
|
|
219
|
+
timestamp=timestamp,
|
|
220
|
+
message_type=msg_type,
|
|
221
|
+
is_final=is_final,
|
|
222
|
+
chunk_type=chunk_type,
|
|
223
|
+
secure_channel_id=decoded.get("secure_channel_id", 0),
|
|
224
|
+
security_token_id=decoded.get("security_token_id"),
|
|
225
|
+
sequence_number=decoded.get("sequence_number"),
|
|
226
|
+
request_id=decoded.get("request_id"),
|
|
227
|
+
service_id=decoded.get("service_id"),
|
|
228
|
+
service_name=self.SERVICE_IDS.get(decoded.get("service_id", 0)),
|
|
229
|
+
payload=payload,
|
|
230
|
+
decoded_service=decoded,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
self.messages.append(message)
|
|
234
|
+
return message
|
|
235
|
+
|
|
236
|
+
def _parse_header(self, data: bytes) -> dict[str, Any]:
|
|
237
|
+
"""Parse message header (8 bytes).
|
|
238
|
+
|
|
239
|
+
Header Format:
|
|
240
|
+
- MessageType (3 bytes) - ASCII characters
|
|
241
|
+
- ChunkType (1 byte) - 'F', 'C', or 'A'
|
|
242
|
+
- MessageSize (4 bytes, little-endian)
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
data: Message data starting with header.
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
Dictionary with header fields.
|
|
249
|
+
|
|
250
|
+
Raises:
|
|
251
|
+
ValueError: If header is invalid.
|
|
252
|
+
"""
|
|
253
|
+
# Parse message type (3 bytes)
|
|
254
|
+
msg_type_bytes = data[0:3]
|
|
255
|
+
msg_type_val = (msg_type_bytes[0] << 16) | (msg_type_bytes[1] << 8) | msg_type_bytes[2]
|
|
256
|
+
msg_type = self.MESSAGE_TYPES.get(msg_type_val)
|
|
257
|
+
|
|
258
|
+
if msg_type is None:
|
|
259
|
+
# Try to decode as ASCII for better error messages
|
|
260
|
+
try:
|
|
261
|
+
msg_type_str = msg_type_bytes.decode("ascii", errors="ignore")
|
|
262
|
+
raise ValueError(
|
|
263
|
+
f"Unknown OPC UA message type: {msg_type_str} (0x{msg_type_val:06X})"
|
|
264
|
+
)
|
|
265
|
+
except UnicodeDecodeError as exc:
|
|
266
|
+
raise ValueError(f"Invalid OPC UA message type: 0x{msg_type_val:06X}") from exc
|
|
267
|
+
|
|
268
|
+
# Parse chunk type
|
|
269
|
+
chunk_type = chr(data[3])
|
|
270
|
+
if chunk_type not in ("F", "C", "A"):
|
|
271
|
+
raise ValueError(f"Invalid chunk type: {chunk_type} (expected F/C/A)")
|
|
272
|
+
|
|
273
|
+
# Parse message size
|
|
274
|
+
message_size = int.from_bytes(data[4:8], "little")
|
|
275
|
+
|
|
276
|
+
if message_size < 8:
|
|
277
|
+
raise ValueError(f"Invalid message size: {message_size} (minimum 8)")
|
|
278
|
+
|
|
279
|
+
if message_size != len(data):
|
|
280
|
+
raise ValueError(f"Message size mismatch: header={message_size}, actual={len(data)}")
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
"message_type": msg_type,
|
|
284
|
+
"chunk_type": chunk_type,
|
|
285
|
+
"message_size": message_size,
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
def _parse_hello(self, data: bytes) -> dict[str, Any]:
|
|
289
|
+
"""Parse Hello message payload.
|
|
290
|
+
|
|
291
|
+
Hello Message Format (after 8-byte header):
|
|
292
|
+
- ProtocolVersion (4 bytes, little-endian)
|
|
293
|
+
- ReceiveBufferSize (4 bytes, little-endian)
|
|
294
|
+
- SendBufferSize (4 bytes, little-endian)
|
|
295
|
+
- MaxMessageSize (4 bytes, little-endian)
|
|
296
|
+
- MaxChunkCount (4 bytes, little-endian)
|
|
297
|
+
- EndpointUrl (String, length-prefixed UTF-8)
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
data: Hello payload (without header).
|
|
301
|
+
|
|
302
|
+
Returns:
|
|
303
|
+
Parsed Hello message data.
|
|
304
|
+
|
|
305
|
+
Raises:
|
|
306
|
+
ValueError: If payload is invalid.
|
|
307
|
+
|
|
308
|
+
Example:
|
|
309
|
+
>>> analyzer = OPCUAAnalyzer()
|
|
310
|
+
>>> payload = bytes([0x00, 0x00, 0x00, 0x00, ...]) # Protocol version 0
|
|
311
|
+
>>> hello = analyzer._parse_hello(payload)
|
|
312
|
+
>>> assert 'protocol_version' in hello
|
|
313
|
+
"""
|
|
314
|
+
if len(data) < 20:
|
|
315
|
+
raise ValueError(f"Hello message too short: {len(data)} bytes (minimum 20)")
|
|
316
|
+
|
|
317
|
+
protocol_version = int.from_bytes(data[0:4], "little")
|
|
318
|
+
receive_buffer_size = int.from_bytes(data[4:8], "little")
|
|
319
|
+
send_buffer_size = int.from_bytes(data[8:12], "little")
|
|
320
|
+
max_message_size = int.from_bytes(data[12:16], "little")
|
|
321
|
+
max_chunk_count = int.from_bytes(data[16:20], "little")
|
|
322
|
+
|
|
323
|
+
# Parse endpoint URL (length-prefixed string)
|
|
324
|
+
endpoint_url = None
|
|
325
|
+
if len(data) >= 24:
|
|
326
|
+
url_str, _ = parse_string(data, 20)
|
|
327
|
+
endpoint_url = url_str
|
|
328
|
+
|
|
329
|
+
return {
|
|
330
|
+
"protocol_version": protocol_version,
|
|
331
|
+
"receive_buffer_size": receive_buffer_size,
|
|
332
|
+
"send_buffer_size": send_buffer_size,
|
|
333
|
+
"max_message_size": max_message_size,
|
|
334
|
+
"max_chunk_count": max_chunk_count,
|
|
335
|
+
"endpoint_url": endpoint_url,
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
def _parse_acknowledge(self, data: bytes) -> dict[str, Any]:
|
|
339
|
+
"""Parse Acknowledge message payload.
|
|
340
|
+
|
|
341
|
+
Acknowledge Message Format:
|
|
342
|
+
- ProtocolVersion (4 bytes)
|
|
343
|
+
- ReceiveBufferSize (4 bytes)
|
|
344
|
+
- SendBufferSize (4 bytes)
|
|
345
|
+
- MaxMessageSize (4 bytes)
|
|
346
|
+
- MaxChunkCount (4 bytes)
|
|
347
|
+
|
|
348
|
+
Args:
|
|
349
|
+
data: Acknowledge payload.
|
|
350
|
+
|
|
351
|
+
Returns:
|
|
352
|
+
Parsed Acknowledge message data.
|
|
353
|
+
"""
|
|
354
|
+
if len(data) < 20:
|
|
355
|
+
raise ValueError(f"Acknowledge message too short: {len(data)} bytes (minimum 20)")
|
|
356
|
+
|
|
357
|
+
return {
|
|
358
|
+
"protocol_version": int.from_bytes(data[0:4], "little"),
|
|
359
|
+
"receive_buffer_size": int.from_bytes(data[4:8], "little"),
|
|
360
|
+
"send_buffer_size": int.from_bytes(data[8:12], "little"),
|
|
361
|
+
"max_message_size": int.from_bytes(data[12:16], "little"),
|
|
362
|
+
"max_chunk_count": int.from_bytes(data[16:20], "little"),
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
def _parse_open_secure_channel(self, data: bytes) -> dict[str, Any]:
|
|
366
|
+
"""Parse Open Secure Channel message payload.
|
|
367
|
+
|
|
368
|
+
OpenSecureChannel Format:
|
|
369
|
+
- SecureChannelId (4 bytes)
|
|
370
|
+
- SecurityPolicyUri (String)
|
|
371
|
+
- ... (complex, varies by security mode)
|
|
372
|
+
|
|
373
|
+
Args:
|
|
374
|
+
data: Open Secure Channel payload.
|
|
375
|
+
|
|
376
|
+
Returns:
|
|
377
|
+
Parsed Open Secure Channel data.
|
|
378
|
+
"""
|
|
379
|
+
if len(data) < 4:
|
|
380
|
+
raise ValueError("OpenSecureChannel message too short")
|
|
381
|
+
|
|
382
|
+
secure_channel_id = int.from_bytes(data[0:4], "little")
|
|
383
|
+
|
|
384
|
+
# Security policy URI at offset 4
|
|
385
|
+
security_policy_uri = None
|
|
386
|
+
if len(data) > 4:
|
|
387
|
+
policy_uri, _ = parse_string(data, 4)
|
|
388
|
+
security_policy_uri = policy_uri
|
|
389
|
+
|
|
390
|
+
return {
|
|
391
|
+
"secure_channel_id": secure_channel_id,
|
|
392
|
+
"security_policy_uri": security_policy_uri,
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
def _parse_message_chunk(self, data: bytes) -> dict[str, Any]:
|
|
396
|
+
"""Parse MSG chunk payload containing service request/response.
|
|
397
|
+
|
|
398
|
+
MSG Chunk Format:
|
|
399
|
+
- SecureChannelId (4 bytes)
|
|
400
|
+
- SecurityTokenId (4 bytes)
|
|
401
|
+
- SequenceNumber (4 bytes)
|
|
402
|
+
- RequestId (4 bytes)
|
|
403
|
+
- Service payload (varies by service type)
|
|
404
|
+
|
|
405
|
+
Args:
|
|
406
|
+
data: MSG chunk payload.
|
|
407
|
+
|
|
408
|
+
Returns:
|
|
409
|
+
Parsed MSG chunk data with service information.
|
|
410
|
+
|
|
411
|
+
Example:
|
|
412
|
+
>>> analyzer = OPCUAAnalyzer()
|
|
413
|
+
>>> # Simplified MSG chunk
|
|
414
|
+
>>> payload = bytes([0x01, 0x00, 0x00, 0x00, ...])
|
|
415
|
+
>>> msg = analyzer._parse_message_chunk(payload)
|
|
416
|
+
>>> assert 'secure_channel_id' in msg
|
|
417
|
+
"""
|
|
418
|
+
if len(data) < 16:
|
|
419
|
+
raise ValueError(f"MSG chunk too short: {len(data)} bytes (minimum 16)")
|
|
420
|
+
|
|
421
|
+
secure_channel_id = int.from_bytes(data[0:4], "little")
|
|
422
|
+
security_token_id = int.from_bytes(data[4:8], "little")
|
|
423
|
+
sequence_number = int.from_bytes(data[8:12], "little")
|
|
424
|
+
request_id = int.from_bytes(data[12:16], "little")
|
|
425
|
+
|
|
426
|
+
# Service payload starts at offset 16
|
|
427
|
+
service_payload = data[16:]
|
|
428
|
+
|
|
429
|
+
# Try to decode service type (NodeId at start of service payload)
|
|
430
|
+
service_id = None
|
|
431
|
+
service_data: dict[str, Any] = {}
|
|
432
|
+
|
|
433
|
+
if len(service_payload) >= 4:
|
|
434
|
+
# Service type is encoded as NodeId
|
|
435
|
+
# For simplicity, assume numeric encoding (most common)
|
|
436
|
+
# Real implementation would use parse_node_id
|
|
437
|
+
try:
|
|
438
|
+
# Try to extract service ID (simplified - assumes FourByte encoding)
|
|
439
|
+
if service_payload[0] == 0x01: # FourByte encoding
|
|
440
|
+
service_id = int.from_bytes(service_payload[2:4], "little")
|
|
441
|
+
service_data = self._decode_service(service_id, service_payload[4:])
|
|
442
|
+
except (ValueError, IndexError):
|
|
443
|
+
service_data = {"raw_payload_size": len(service_payload)}
|
|
444
|
+
|
|
445
|
+
return {
|
|
446
|
+
"secure_channel_id": secure_channel_id,
|
|
447
|
+
"security_token_id": security_token_id,
|
|
448
|
+
"sequence_number": sequence_number,
|
|
449
|
+
"request_id": request_id,
|
|
450
|
+
"service_id": service_id,
|
|
451
|
+
"service_data": service_data,
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
def _parse_error(self, data: bytes) -> dict[str, Any]:
|
|
455
|
+
"""Parse Error message payload.
|
|
456
|
+
|
|
457
|
+
Error Message Format:
|
|
458
|
+
- Error (4 bytes, StatusCode)
|
|
459
|
+
- Reason (String)
|
|
460
|
+
|
|
461
|
+
Args:
|
|
462
|
+
data: Error payload.
|
|
463
|
+
|
|
464
|
+
Returns:
|
|
465
|
+
Parsed error data.
|
|
466
|
+
"""
|
|
467
|
+
if len(data) < 4:
|
|
468
|
+
raise ValueError("Error message too short")
|
|
469
|
+
|
|
470
|
+
error_code = int.from_bytes(data[0:4], "little")
|
|
471
|
+
|
|
472
|
+
# Parse reason string
|
|
473
|
+
reason = None
|
|
474
|
+
if len(data) > 4:
|
|
475
|
+
reason_str, _ = parse_string(data, 4)
|
|
476
|
+
reason = reason_str
|
|
477
|
+
|
|
478
|
+
return {
|
|
479
|
+
"error_code": error_code,
|
|
480
|
+
"reason": reason,
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
def _decode_service(self, service_id: int, payload: bytes) -> dict[str, Any]:
|
|
484
|
+
"""Decode service-specific payload.
|
|
485
|
+
|
|
486
|
+
Args:
|
|
487
|
+
service_id: Service type identifier.
|
|
488
|
+
payload: Service payload bytes.
|
|
489
|
+
|
|
490
|
+
Returns:
|
|
491
|
+
Decoded service data.
|
|
492
|
+
|
|
493
|
+
Example:
|
|
494
|
+
>>> analyzer = OPCUAAnalyzer()
|
|
495
|
+
>>> # ReadRequest service
|
|
496
|
+
>>> service_data = analyzer._decode_service(421, b'...')
|
|
497
|
+
>>> assert 'service' in service_data
|
|
498
|
+
"""
|
|
499
|
+
# Check if we have a parser for this service
|
|
500
|
+
if service_id in SERVICE_PARSERS:
|
|
501
|
+
parser_request, parser_response = SERVICE_PARSERS[service_id]
|
|
502
|
+
try:
|
|
503
|
+
# Try request parser first (would need context to determine)
|
|
504
|
+
if parser_request is not None:
|
|
505
|
+
result: dict[str, Any] = parser_request(payload)
|
|
506
|
+
return result
|
|
507
|
+
except (ValueError, IndexError):
|
|
508
|
+
pass
|
|
509
|
+
|
|
510
|
+
# Fallback to basic info
|
|
511
|
+
fallback: dict[str, Any] = {
|
|
512
|
+
"service_id": service_id,
|
|
513
|
+
"service_name": self.SERVICE_IDS.get(service_id, "Unknown"),
|
|
514
|
+
"payload_size": len(payload),
|
|
515
|
+
}
|
|
516
|
+
return fallback
|
|
517
|
+
|
|
518
|
+
def export_address_space(self, output_path: Path) -> None:
|
|
519
|
+
"""Export discovered address space as JSON.
|
|
520
|
+
|
|
521
|
+
Exports all discovered nodes with their properties to a JSON file.
|
|
522
|
+
|
|
523
|
+
Args:
|
|
524
|
+
output_path: Path to output JSON file.
|
|
525
|
+
|
|
526
|
+
Example:
|
|
527
|
+
>>> analyzer = OPCUAAnalyzer()
|
|
528
|
+
>>> # ... parse messages and discover nodes ...
|
|
529
|
+
>>> analyzer.export_address_space(Path("opcua_nodes.json"))
|
|
530
|
+
"""
|
|
531
|
+
export_data = {
|
|
532
|
+
"nodes": [
|
|
533
|
+
{
|
|
534
|
+
"node_id": node.node_id,
|
|
535
|
+
"node_class": node.node_class,
|
|
536
|
+
"browse_name": node.browse_name,
|
|
537
|
+
"display_name": node.display_name,
|
|
538
|
+
"value": str(node.value) if node.value is not None else None,
|
|
539
|
+
"data_type": node.data_type,
|
|
540
|
+
"children": node.children,
|
|
541
|
+
}
|
|
542
|
+
for node in self.nodes.values()
|
|
543
|
+
],
|
|
544
|
+
"message_count": len(self.messages),
|
|
545
|
+
"security_mode": self.security_mode,
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
with output_path.open("w") as f:
|
|
549
|
+
json.dump(export_data, f, indent=2)
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
__all__ = ["OPCUAAnalyzer", "OPCUAMessage", "OPCUANode"]
|