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,850 @@
|
|
|
1
|
+
"""Interactive web dashboard for protocol analysis.
|
|
2
|
+
|
|
3
|
+
This module provides a comprehensive web-based UI for Oscura, enabling:
|
|
4
|
+
- File upload and analysis management
|
|
5
|
+
- Real-time progress tracking
|
|
6
|
+
- Interactive waveform visualization
|
|
7
|
+
- Protocol exploration and browsing
|
|
8
|
+
- Export to multiple formats (Wireshark, Scapy, Kaitai)
|
|
9
|
+
- Session management interface
|
|
10
|
+
|
|
11
|
+
The dashboard uses FastAPI for the backend with Jinja2 templates for
|
|
12
|
+
HTML rendering. Real-time updates are provided via WebSocket connections.
|
|
13
|
+
|
|
14
|
+
Example:
|
|
15
|
+
>>> from oscura.api.server import WebDashboard
|
|
16
|
+
>>> dashboard = WebDashboard(host="0.0.0.0", port=5000)
|
|
17
|
+
>>> dashboard.run() # Starts server on http://0.0.0.0:5000
|
|
18
|
+
|
|
19
|
+
Architecture:
|
|
20
|
+
- FastAPI backend (REST API + WebSocket)
|
|
21
|
+
- Jinja2 templates for HTML pages
|
|
22
|
+
- Bootstrap CSS framework for responsive UI
|
|
23
|
+
- Plotly.js for interactive waveform visualization
|
|
24
|
+
- Vanilla JavaScript for client-side interactivity
|
|
25
|
+
- Optional dark/light theme support
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import json
|
|
31
|
+
import logging
|
|
32
|
+
from dataclasses import dataclass, field
|
|
33
|
+
from datetime import datetime
|
|
34
|
+
from pathlib import Path
|
|
35
|
+
from typing import Any
|
|
36
|
+
|
|
37
|
+
logger = logging.getLogger(__name__)
|
|
38
|
+
|
|
39
|
+
# Try to import FastAPI and dependencies
|
|
40
|
+
try:
|
|
41
|
+
from fastapi import (
|
|
42
|
+
BackgroundTasks,
|
|
43
|
+
FastAPI,
|
|
44
|
+
HTTPException,
|
|
45
|
+
Request,
|
|
46
|
+
UploadFile,
|
|
47
|
+
WebSocket,
|
|
48
|
+
WebSocketDisconnect,
|
|
49
|
+
status,
|
|
50
|
+
)
|
|
51
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
52
|
+
from fastapi.responses import (
|
|
53
|
+
FileResponse,
|
|
54
|
+
HTMLResponse,
|
|
55
|
+
JSONResponse,
|
|
56
|
+
)
|
|
57
|
+
from fastapi.staticfiles import StaticFiles
|
|
58
|
+
from fastapi.templating import Jinja2Templates
|
|
59
|
+
|
|
60
|
+
HAS_FASTAPI = True
|
|
61
|
+
except ImportError:
|
|
62
|
+
HAS_FASTAPI = False
|
|
63
|
+
logger.warning("FastAPI not available. Install with: pip install 'fastapi[all]' uvicorn")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# ============================================================================
|
|
67
|
+
# Dashboard Configuration
|
|
68
|
+
# ============================================================================
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass
|
|
72
|
+
class DashboardConfig:
|
|
73
|
+
"""Configuration for web dashboard.
|
|
74
|
+
|
|
75
|
+
Attributes:
|
|
76
|
+
title: Dashboard title displayed in UI.
|
|
77
|
+
theme: Default theme (light or dark).
|
|
78
|
+
max_file_size: Maximum upload file size in bytes.
|
|
79
|
+
enable_websocket: Enable WebSocket for real-time updates.
|
|
80
|
+
session_timeout: Session timeout in seconds.
|
|
81
|
+
cache_waveforms: Cache waveform data for faster display.
|
|
82
|
+
plotly_config: Configuration for Plotly.js charts.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
title: str = "Oscura Protocol Analysis Dashboard"
|
|
86
|
+
theme: str = "dark"
|
|
87
|
+
max_file_size: int = 100 * 1024 * 1024 # 100 MB
|
|
88
|
+
enable_websocket: bool = True
|
|
89
|
+
session_timeout: float = 3600.0 # 1 hour
|
|
90
|
+
cache_waveforms: bool = True
|
|
91
|
+
plotly_config: dict[str, Any] = field(
|
|
92
|
+
default_factory=lambda: {
|
|
93
|
+
"responsive": True,
|
|
94
|
+
"displayModeBar": True,
|
|
95
|
+
"displaylogo": False,
|
|
96
|
+
"toImageButtonOptions": {
|
|
97
|
+
"format": "png",
|
|
98
|
+
"filename": "oscura_waveform",
|
|
99
|
+
"height": 800,
|
|
100
|
+
"width": 1200,
|
|
101
|
+
},
|
|
102
|
+
}
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# ============================================================================
|
|
107
|
+
# WebSocket Connection Manager
|
|
108
|
+
# ============================================================================
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class ConnectionManager:
|
|
112
|
+
"""Manages WebSocket connections for real-time updates.
|
|
113
|
+
|
|
114
|
+
Attributes:
|
|
115
|
+
active_connections: Dict mapping session_id to WebSocket connections.
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
def __init__(self) -> None:
|
|
119
|
+
"""Initialize connection manager."""
|
|
120
|
+
self.active_connections: dict[str, list[WebSocket]] = {}
|
|
121
|
+
|
|
122
|
+
async def connect(self, websocket: WebSocket, session_id: str) -> None:
|
|
123
|
+
"""Accept WebSocket connection.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
websocket: WebSocket connection.
|
|
127
|
+
session_id: Session identifier.
|
|
128
|
+
"""
|
|
129
|
+
await websocket.accept()
|
|
130
|
+
if session_id not in self.active_connections:
|
|
131
|
+
self.active_connections[session_id] = []
|
|
132
|
+
self.active_connections[session_id].append(websocket)
|
|
133
|
+
logger.info(f"WebSocket connected for session {session_id}")
|
|
134
|
+
|
|
135
|
+
def disconnect(self, websocket: WebSocket, session_id: str) -> None:
|
|
136
|
+
"""Remove WebSocket connection.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
websocket: WebSocket connection.
|
|
140
|
+
session_id: Session identifier.
|
|
141
|
+
"""
|
|
142
|
+
if session_id in self.active_connections:
|
|
143
|
+
self.active_connections[session_id].remove(websocket)
|
|
144
|
+
if not self.active_connections[session_id]:
|
|
145
|
+
del self.active_connections[session_id]
|
|
146
|
+
logger.info(f"WebSocket disconnected for session {session_id}")
|
|
147
|
+
|
|
148
|
+
async def send_message(self, session_id: str, message: dict[str, Any]) -> None:
|
|
149
|
+
"""Send message to all connections for a session.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
session_id: Session identifier.
|
|
153
|
+
message: Message to send (will be JSON-encoded).
|
|
154
|
+
"""
|
|
155
|
+
if session_id in self.active_connections:
|
|
156
|
+
for connection in self.active_connections[session_id]:
|
|
157
|
+
try:
|
|
158
|
+
await connection.send_json(message)
|
|
159
|
+
except Exception as e:
|
|
160
|
+
logger.error(f"Failed to send WebSocket message: {e}")
|
|
161
|
+
|
|
162
|
+
async def broadcast(self, message: dict[str, Any]) -> None:
|
|
163
|
+
"""Broadcast message to all active connections.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
message: Message to send (will be JSON-encoded).
|
|
167
|
+
"""
|
|
168
|
+
for connections in self.active_connections.values():
|
|
169
|
+
for connection in connections:
|
|
170
|
+
try:
|
|
171
|
+
await connection.send_json(message)
|
|
172
|
+
except Exception as e:
|
|
173
|
+
logger.error(f"Failed to broadcast WebSocket message: {e}")
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
# ============================================================================
|
|
177
|
+
# Web Dashboard
|
|
178
|
+
# ============================================================================
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class WebDashboard:
|
|
182
|
+
"""Interactive web dashboard for Oscura protocol analysis.
|
|
183
|
+
|
|
184
|
+
Provides comprehensive web-based UI for:
|
|
185
|
+
- File upload and analysis management
|
|
186
|
+
- Real-time progress tracking via WebSocket
|
|
187
|
+
- Interactive waveform visualization with Plotly.js
|
|
188
|
+
- Protocol exploration and browsing
|
|
189
|
+
- Export to multiple formats
|
|
190
|
+
- Session management interface
|
|
191
|
+
- Dark/light theme toggle
|
|
192
|
+
- Responsive mobile-friendly design
|
|
193
|
+
|
|
194
|
+
Example:
|
|
195
|
+
>>> dashboard = WebDashboard(host="0.0.0.0", port=5000)
|
|
196
|
+
>>> dashboard.run()
|
|
197
|
+
>>> # Visit http://0.0.0.0:5000 for web UI
|
|
198
|
+
|
|
199
|
+
Architecture:
|
|
200
|
+
- FastAPI backend for REST API endpoints
|
|
201
|
+
- Jinja2 templates for server-side rendering
|
|
202
|
+
- WebSocket for real-time progress updates
|
|
203
|
+
- Bootstrap for responsive CSS framework
|
|
204
|
+
- Plotly.js for interactive charts
|
|
205
|
+
- Vanilla JavaScript for client-side logic
|
|
206
|
+
"""
|
|
207
|
+
|
|
208
|
+
def __init__(
|
|
209
|
+
self,
|
|
210
|
+
host: str = "127.0.0.1",
|
|
211
|
+
port: int = 5000,
|
|
212
|
+
config: DashboardConfig | None = None,
|
|
213
|
+
api_server: Any | None = None,
|
|
214
|
+
):
|
|
215
|
+
"""Initialize web dashboard.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
host: Server host address.
|
|
219
|
+
port: Server port number.
|
|
220
|
+
config: Dashboard configuration.
|
|
221
|
+
api_server: Optional RESTAPIServer instance (for session management).
|
|
222
|
+
|
|
223
|
+
Raises:
|
|
224
|
+
ImportError: If FastAPI is not available.
|
|
225
|
+
"""
|
|
226
|
+
if not HAS_FASTAPI:
|
|
227
|
+
raise ImportError("FastAPI required. Install with: pip install 'fastapi[all]' uvicorn")
|
|
228
|
+
|
|
229
|
+
self.host = host
|
|
230
|
+
self.port = port
|
|
231
|
+
self.config = config or DashboardConfig()
|
|
232
|
+
|
|
233
|
+
# Import REST API server for session management
|
|
234
|
+
if api_server is None:
|
|
235
|
+
from oscura.api.rest_server import RESTAPIServer
|
|
236
|
+
|
|
237
|
+
self.api_server = RESTAPIServer(host=host, port=8000)
|
|
238
|
+
else:
|
|
239
|
+
self.api_server = api_server
|
|
240
|
+
|
|
241
|
+
# WebSocket connection manager
|
|
242
|
+
self.ws_manager = ConnectionManager()
|
|
243
|
+
|
|
244
|
+
# Create FastAPI app with dynamic version from package metadata (SSOT: pyproject.toml)
|
|
245
|
+
try:
|
|
246
|
+
from importlib.metadata import version
|
|
247
|
+
|
|
248
|
+
app_version = version("oscura")
|
|
249
|
+
except Exception:
|
|
250
|
+
app_version = "0.0.0+dev"
|
|
251
|
+
|
|
252
|
+
self.app = FastAPI(
|
|
253
|
+
title=self.config.title,
|
|
254
|
+
description="Interactive web dashboard for hardware reverse engineering",
|
|
255
|
+
version=app_version,
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
# Add CORS middleware
|
|
259
|
+
self.app.add_middleware(
|
|
260
|
+
CORSMiddleware,
|
|
261
|
+
allow_origins=["*"],
|
|
262
|
+
allow_credentials=True,
|
|
263
|
+
allow_methods=["*"],
|
|
264
|
+
allow_headers=["*"],
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
# Setup templates and static files
|
|
268
|
+
self._setup_templates()
|
|
269
|
+
|
|
270
|
+
# Register routes
|
|
271
|
+
self._register_routes()
|
|
272
|
+
|
|
273
|
+
def _setup_templates(self) -> None:
|
|
274
|
+
"""Setup Jinja2 templates and static files."""
|
|
275
|
+
# Get path to web module
|
|
276
|
+
web_dir = Path(__file__).parent
|
|
277
|
+
|
|
278
|
+
# Templates directory
|
|
279
|
+
templates_dir = web_dir / "templates"
|
|
280
|
+
if not templates_dir.exists():
|
|
281
|
+
templates_dir.mkdir(parents=True)
|
|
282
|
+
logger.warning(f"Templates directory created: {templates_dir}")
|
|
283
|
+
|
|
284
|
+
self.templates = Jinja2Templates(directory=str(templates_dir))
|
|
285
|
+
|
|
286
|
+
# Static files directory
|
|
287
|
+
static_dir = web_dir / "static"
|
|
288
|
+
if not static_dir.exists():
|
|
289
|
+
static_dir.mkdir(parents=True)
|
|
290
|
+
logger.warning(f"Static directory created: {static_dir}")
|
|
291
|
+
|
|
292
|
+
# Mount static files
|
|
293
|
+
try:
|
|
294
|
+
self.app.mount("/static", StaticFiles(directory=str(static_dir)), name="static")
|
|
295
|
+
except Exception as e:
|
|
296
|
+
logger.warning(f"Could not mount static files: {e}")
|
|
297
|
+
|
|
298
|
+
def _register_routes(self) -> None:
|
|
299
|
+
"""Register all dashboard routes and endpoints."""
|
|
300
|
+
self._register_page_routes()
|
|
301
|
+
self._register_api_routes()
|
|
302
|
+
self._register_websocket_routes()
|
|
303
|
+
|
|
304
|
+
def _register_page_routes(self) -> None:
|
|
305
|
+
"""Register HTML page routes."""
|
|
306
|
+
self.app.get("/", response_class=HTMLResponse, tags=["Dashboard"])(self._route_home)
|
|
307
|
+
self.app.get("/sessions", response_class=HTMLResponse, tags=["Dashboard"])(
|
|
308
|
+
self._route_sessions
|
|
309
|
+
)
|
|
310
|
+
self.app.get("/session/{session_id}", response_class=HTMLResponse, tags=["Dashboard"])(
|
|
311
|
+
self._route_session_detail
|
|
312
|
+
)
|
|
313
|
+
self.app.get("/protocols", response_class=HTMLResponse, tags=["Dashboard"])(
|
|
314
|
+
self._route_protocols
|
|
315
|
+
)
|
|
316
|
+
self.app.get("/waveforms/{session_id}", response_class=HTMLResponse, tags=["Dashboard"])(
|
|
317
|
+
self._route_waveforms
|
|
318
|
+
)
|
|
319
|
+
self.app.get("/reports/{session_id}", response_class=HTMLResponse, tags=["Dashboard"])(
|
|
320
|
+
self._route_reports
|
|
321
|
+
)
|
|
322
|
+
self.app.get("/export/{session_id}", response_class=HTMLResponse, tags=["Dashboard"])(
|
|
323
|
+
self._route_export
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
def _base_context(self, request: Request, **kwargs: Any) -> dict[str, Any]:
|
|
327
|
+
"""Build base template context."""
|
|
328
|
+
return {
|
|
329
|
+
"request": request,
|
|
330
|
+
"title": self.config.title,
|
|
331
|
+
"theme": self.config.theme,
|
|
332
|
+
**kwargs,
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
async def _route_home(self, request: Request) -> HTMLResponse:
|
|
336
|
+
"""Dashboard home page with file upload."""
|
|
337
|
+
return self.templates.TemplateResponse(
|
|
338
|
+
"home.html",
|
|
339
|
+
self._base_context(
|
|
340
|
+
request, max_file_size_mb=self.config.max_file_size // (1024 * 1024)
|
|
341
|
+
),
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
async def _route_sessions(self, request: Request) -> HTMLResponse:
|
|
345
|
+
"""Sessions management page."""
|
|
346
|
+
return self.templates.TemplateResponse(
|
|
347
|
+
"sessions.html",
|
|
348
|
+
self._base_context(request, sessions=self.api_server.session_manager.list_sessions()),
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
async def _route_session_detail(self, request: Request, session_id: str) -> HTMLResponse:
|
|
352
|
+
"""Session detail page with analysis results."""
|
|
353
|
+
session = self._get_session_or_404(session_id)
|
|
354
|
+
return self.templates.TemplateResponse(
|
|
355
|
+
"session_detail.html",
|
|
356
|
+
self._base_context(
|
|
357
|
+
request, session=session, protocol_spec=self._extract_protocol_spec(session)
|
|
358
|
+
),
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
async def _route_protocols(self, request: Request) -> HTMLResponse:
|
|
362
|
+
"""Protocols browser page."""
|
|
363
|
+
return self.templates.TemplateResponse(
|
|
364
|
+
"protocols.html", self._base_context(request, protocols=self._gather_protocol_list())
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
async def _route_waveforms(self, request: Request, session_id: str) -> HTMLResponse:
|
|
368
|
+
"""Interactive waveform viewer page."""
|
|
369
|
+
session = self._get_session_or_404(session_id)
|
|
370
|
+
return self.templates.TemplateResponse(
|
|
371
|
+
"waveforms.html",
|
|
372
|
+
self._base_context(
|
|
373
|
+
request,
|
|
374
|
+
session_id=session_id,
|
|
375
|
+
filename=session["filename"],
|
|
376
|
+
plotly_config=json.dumps(self.config.plotly_config),
|
|
377
|
+
),
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
async def _route_reports(self, request: Request, session_id: str) -> HTMLResponse:
|
|
381
|
+
"""Analysis reports page."""
|
|
382
|
+
session = self._get_session_or_404(session_id)
|
|
383
|
+
report_path = self._extract_report_path(session)
|
|
384
|
+
return self.templates.TemplateResponse(
|
|
385
|
+
"reports.html",
|
|
386
|
+
self._base_context(
|
|
387
|
+
request,
|
|
388
|
+
session_id=session_id,
|
|
389
|
+
report_path=str(report_path) if report_path else None,
|
|
390
|
+
),
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
async def _route_export(self, request: Request, session_id: str) -> HTMLResponse:
|
|
394
|
+
"""Export/download page for generated artifacts."""
|
|
395
|
+
session = self._get_session_or_404(session_id)
|
|
396
|
+
return self.templates.TemplateResponse(
|
|
397
|
+
"export.html",
|
|
398
|
+
self._base_context(
|
|
399
|
+
request, session_id=session_id, artifacts=self._extract_artifacts(session)
|
|
400
|
+
),
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
def _register_api_routes(self) -> None:
|
|
404
|
+
"""Register API endpoints for AJAX requests."""
|
|
405
|
+
|
|
406
|
+
@self.app.post("/api/upload", tags=["API"])
|
|
407
|
+
async def upload_file(
|
|
408
|
+
file: UploadFile,
|
|
409
|
+
background_tasks: BackgroundTasks,
|
|
410
|
+
protocol_hint: str | None = None,
|
|
411
|
+
auto_crc: bool = True,
|
|
412
|
+
detect_crypto: bool = True,
|
|
413
|
+
generate_tests: bool = True,
|
|
414
|
+
) -> JSONResponse:
|
|
415
|
+
"""Upload file and start analysis."""
|
|
416
|
+
self._validate_upload_file(file)
|
|
417
|
+
file_data = await self._read_and_validate_file_size(file)
|
|
418
|
+
|
|
419
|
+
options = {
|
|
420
|
+
"protocol_hint": protocol_hint,
|
|
421
|
+
"auto_crc": auto_crc,
|
|
422
|
+
"detect_crypto": detect_crypto,
|
|
423
|
+
"generate_tests": generate_tests,
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
session_id = self._create_analysis_session(file.filename, file_data, options)
|
|
427
|
+
|
|
428
|
+
# Add background task for analysis (FastAPI auto-injects BackgroundTasks)
|
|
429
|
+
background_tasks.add_task(self._run_analysis_with_updates, session_id)
|
|
430
|
+
|
|
431
|
+
return JSONResponse(
|
|
432
|
+
{
|
|
433
|
+
"session_id": session_id,
|
|
434
|
+
"status": "processing",
|
|
435
|
+
"message": "Analysis started",
|
|
436
|
+
"created_at": datetime.utcnow().isoformat(),
|
|
437
|
+
}
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
@self.app.get("/api/session/{session_id}/status", tags=["API"])
|
|
441
|
+
async def get_session_status(session_id: str) -> JSONResponse:
|
|
442
|
+
"""Get session status for AJAX polling."""
|
|
443
|
+
session = self._get_session_or_404(session_id)
|
|
444
|
+
return JSONResponse(
|
|
445
|
+
{
|
|
446
|
+
"session_id": session_id,
|
|
447
|
+
"status": session["status"],
|
|
448
|
+
"updated_at": session["updated_at"],
|
|
449
|
+
"error": session.get("error"),
|
|
450
|
+
}
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
@self.app.get("/api/session/{session_id}/waveform", tags=["API"])
|
|
454
|
+
async def get_waveform_data(session_id: str) -> JSONResponse:
|
|
455
|
+
"""Get waveform data for Plotly.js visualization."""
|
|
456
|
+
session = self._get_session_or_404(session_id)
|
|
457
|
+
waveform_data = self._generate_waveform_data(session)
|
|
458
|
+
return JSONResponse(waveform_data)
|
|
459
|
+
|
|
460
|
+
@self.app.delete("/api/session/{session_id}", tags=["API"])
|
|
461
|
+
async def delete_session_api(session_id: str) -> JSONResponse:
|
|
462
|
+
"""Delete session via AJAX."""
|
|
463
|
+
deleted = self.api_server.session_manager.delete_session(session_id)
|
|
464
|
+
if not deleted:
|
|
465
|
+
raise HTTPException(
|
|
466
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
467
|
+
detail=f"Session {session_id} not found",
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
return JSONResponse(
|
|
471
|
+
{
|
|
472
|
+
"message": "Session deleted",
|
|
473
|
+
"session_id": session_id,
|
|
474
|
+
"timestamp": datetime.utcnow().isoformat(),
|
|
475
|
+
}
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
@self.app.get("/api/download/{session_id}/{artifact_type}", tags=["API"])
|
|
479
|
+
async def download_artifact(session_id: str, artifact_type: str) -> FileResponse:
|
|
480
|
+
"""Download generated artifact."""
|
|
481
|
+
session = self._get_session_or_404(session_id)
|
|
482
|
+
artifact_path = self._get_artifact_path(session, artifact_type)
|
|
483
|
+
return FileResponse(
|
|
484
|
+
path=str(artifact_path),
|
|
485
|
+
filename=Path(artifact_path).name,
|
|
486
|
+
media_type="application/octet-stream",
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
def _register_websocket_routes(self) -> None:
|
|
490
|
+
"""Register WebSocket endpoints for real-time updates."""
|
|
491
|
+
|
|
492
|
+
@self.app.websocket("/ws/{session_id}")
|
|
493
|
+
async def websocket_endpoint(websocket: WebSocket, session_id: str) -> None:
|
|
494
|
+
"""WebSocket connection for real-time analysis updates."""
|
|
495
|
+
await self.ws_manager.connect(websocket, session_id)
|
|
496
|
+
try:
|
|
497
|
+
while True:
|
|
498
|
+
data = await websocket.receive_text()
|
|
499
|
+
logger.debug(f"WebSocket received: {data}")
|
|
500
|
+
except WebSocketDisconnect:
|
|
501
|
+
self.ws_manager.disconnect(websocket, session_id)
|
|
502
|
+
|
|
503
|
+
def _get_session_or_404(self, session_id: str) -> dict[str, Any]:
|
|
504
|
+
"""Get session or raise 404 error.
|
|
505
|
+
|
|
506
|
+
Args:
|
|
507
|
+
session_id: Session identifier.
|
|
508
|
+
|
|
509
|
+
Returns:
|
|
510
|
+
Session data dictionary.
|
|
511
|
+
|
|
512
|
+
Raises:
|
|
513
|
+
HTTPException: If session not found.
|
|
514
|
+
"""
|
|
515
|
+
session = self.api_server.session_manager.get_session(session_id)
|
|
516
|
+
if not session:
|
|
517
|
+
raise HTTPException(
|
|
518
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
519
|
+
detail=f"Session {session_id} not found",
|
|
520
|
+
)
|
|
521
|
+
return session
|
|
522
|
+
|
|
523
|
+
def _extract_protocol_spec(self, session: dict[str, Any]) -> dict[str, Any] | None:
|
|
524
|
+
"""Extract protocol specification from session result.
|
|
525
|
+
|
|
526
|
+
Args:
|
|
527
|
+
session: Session data dictionary.
|
|
528
|
+
|
|
529
|
+
Returns:
|
|
530
|
+
Serialized protocol spec or None.
|
|
531
|
+
"""
|
|
532
|
+
if session["result"]:
|
|
533
|
+
return self.api_server._serialize_protocol_spec(session["result"])
|
|
534
|
+
return None
|
|
535
|
+
|
|
536
|
+
def _extract_report_path(self, session: dict[str, Any]) -> Path | None:
|
|
537
|
+
"""Extract report path from session result.
|
|
538
|
+
|
|
539
|
+
Args:
|
|
540
|
+
session: Session data dictionary.
|
|
541
|
+
|
|
542
|
+
Returns:
|
|
543
|
+
Report path or None.
|
|
544
|
+
"""
|
|
545
|
+
if session["result"]:
|
|
546
|
+
result = session["result"]
|
|
547
|
+
return getattr(result, "report_path", None)
|
|
548
|
+
return None
|
|
549
|
+
|
|
550
|
+
def _extract_artifacts(self, session: dict[str, Any]) -> dict[str, Any]:
|
|
551
|
+
"""Extract artifacts from session result.
|
|
552
|
+
|
|
553
|
+
Args:
|
|
554
|
+
session: Session data dictionary.
|
|
555
|
+
|
|
556
|
+
Returns:
|
|
557
|
+
Dictionary of artifacts.
|
|
558
|
+
"""
|
|
559
|
+
if session["result"]:
|
|
560
|
+
return self.api_server._serialize_artifacts(session["result"])
|
|
561
|
+
return {}
|
|
562
|
+
|
|
563
|
+
def _gather_protocol_list(self) -> list[dict[str, Any]]:
|
|
564
|
+
"""Gather list of protocols from all sessions.
|
|
565
|
+
|
|
566
|
+
Returns:
|
|
567
|
+
List of protocol information dictionaries.
|
|
568
|
+
"""
|
|
569
|
+
protocols = []
|
|
570
|
+
for session in self.api_server.session_manager.sessions.values():
|
|
571
|
+
if session["result"]:
|
|
572
|
+
result = session["result"]
|
|
573
|
+
spec = getattr(result, "protocol_spec", None)
|
|
574
|
+
if spec:
|
|
575
|
+
protocols.append(
|
|
576
|
+
{
|
|
577
|
+
"session_id": session["id"],
|
|
578
|
+
"protocol_name": getattr(spec, "protocol_name", "unknown"),
|
|
579
|
+
"confidence": getattr(result, "confidence_score", 0.0),
|
|
580
|
+
"message_count": len(getattr(spec, "messages", [])),
|
|
581
|
+
"field_count": len(getattr(spec, "fields", [])),
|
|
582
|
+
"filename": session["filename"],
|
|
583
|
+
"created_at": session["created_at"],
|
|
584
|
+
}
|
|
585
|
+
)
|
|
586
|
+
return protocols
|
|
587
|
+
|
|
588
|
+
def _validate_upload_file(self, file: UploadFile) -> None:
|
|
589
|
+
"""Validate uploaded file has a filename.
|
|
590
|
+
|
|
591
|
+
Args:
|
|
592
|
+
file: Uploaded file object.
|
|
593
|
+
|
|
594
|
+
Raises:
|
|
595
|
+
HTTPException: If filename is missing.
|
|
596
|
+
"""
|
|
597
|
+
if not file.filename:
|
|
598
|
+
raise HTTPException(
|
|
599
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
600
|
+
detail="Filename required",
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
async def _read_and_validate_file_size(self, file: UploadFile) -> bytes:
|
|
604
|
+
"""Read and validate file size.
|
|
605
|
+
|
|
606
|
+
Args:
|
|
607
|
+
file: Uploaded file object.
|
|
608
|
+
|
|
609
|
+
Returns:
|
|
610
|
+
File data as bytes.
|
|
611
|
+
|
|
612
|
+
Raises:
|
|
613
|
+
HTTPException: If file exceeds maximum size.
|
|
614
|
+
"""
|
|
615
|
+
file_data_raw = await file.read()
|
|
616
|
+
# Ensure we have bytes (UploadFile.read() returns bytes | str based on mode)
|
|
617
|
+
file_data = file_data_raw if isinstance(file_data_raw, bytes) else file_data_raw.encode()
|
|
618
|
+
if len(file_data) > self.config.max_file_size:
|
|
619
|
+
raise HTTPException(
|
|
620
|
+
status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
|
|
621
|
+
detail=f"File too large (max {self.config.max_file_size} bytes)",
|
|
622
|
+
)
|
|
623
|
+
return file_data
|
|
624
|
+
|
|
625
|
+
def _create_analysis_session(
|
|
626
|
+
self, filename: str | None, file_data: bytes, options: dict[str, Any]
|
|
627
|
+
) -> str:
|
|
628
|
+
"""Create analysis session via API server.
|
|
629
|
+
|
|
630
|
+
Args:
|
|
631
|
+
filename: Name of uploaded file.
|
|
632
|
+
file_data: File content as bytes.
|
|
633
|
+
options: Analysis options dictionary.
|
|
634
|
+
|
|
635
|
+
Returns:
|
|
636
|
+
Session ID string.
|
|
637
|
+
|
|
638
|
+
Raises:
|
|
639
|
+
HTTPException: If session creation fails or filename is None.
|
|
640
|
+
"""
|
|
641
|
+
if not filename:
|
|
642
|
+
raise HTTPException(
|
|
643
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
644
|
+
detail="Filename required",
|
|
645
|
+
)
|
|
646
|
+
try:
|
|
647
|
+
return self.api_server.session_manager.create_session(filename, file_data, options)
|
|
648
|
+
except RuntimeError as e:
|
|
649
|
+
raise HTTPException(
|
|
650
|
+
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
651
|
+
detail=str(e),
|
|
652
|
+
) from e
|
|
653
|
+
|
|
654
|
+
def _get_artifact_path(self, session: dict[str, Any], artifact_type: str) -> Path:
|
|
655
|
+
"""Get path to specific artifact type.
|
|
656
|
+
|
|
657
|
+
Args:
|
|
658
|
+
session: Session data dictionary.
|
|
659
|
+
artifact_type: Type of artifact (dissector, scapy, kaitai, report, tests).
|
|
660
|
+
|
|
661
|
+
Returns:
|
|
662
|
+
Path to artifact file.
|
|
663
|
+
|
|
664
|
+
Raises:
|
|
665
|
+
HTTPException: If artifact type invalid, result missing, or file not found.
|
|
666
|
+
"""
|
|
667
|
+
if not session["result"]:
|
|
668
|
+
raise HTTPException(
|
|
669
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
670
|
+
detail="No results available",
|
|
671
|
+
)
|
|
672
|
+
|
|
673
|
+
artifact_map = {
|
|
674
|
+
"dissector": "dissector_path",
|
|
675
|
+
"scapy": "scapy_layer_path",
|
|
676
|
+
"kaitai": "kaitai_path",
|
|
677
|
+
"report": "report_path",
|
|
678
|
+
"tests": "test_vectors_path",
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
if artifact_type not in artifact_map:
|
|
682
|
+
raise HTTPException(
|
|
683
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
684
|
+
detail=f"Invalid artifact type: {artifact_type}",
|
|
685
|
+
)
|
|
686
|
+
|
|
687
|
+
result = session["result"]
|
|
688
|
+
artifact_path = getattr(result, artifact_map[artifact_type], None)
|
|
689
|
+
|
|
690
|
+
if not artifact_path or not Path(artifact_path).exists():
|
|
691
|
+
raise HTTPException(
|
|
692
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
693
|
+
detail=f"Artifact not found: {artifact_type}",
|
|
694
|
+
)
|
|
695
|
+
|
|
696
|
+
return Path(artifact_path)
|
|
697
|
+
|
|
698
|
+
async def _run_analysis_with_updates(self, session_id: str) -> None:
|
|
699
|
+
"""Run analysis with WebSocket progress updates.
|
|
700
|
+
|
|
701
|
+
Args:
|
|
702
|
+
session_id: Session identifier.
|
|
703
|
+
"""
|
|
704
|
+
# Send initial status
|
|
705
|
+
await self.ws_manager.send_message(
|
|
706
|
+
session_id,
|
|
707
|
+
{
|
|
708
|
+
"type": "status",
|
|
709
|
+
"status": "processing",
|
|
710
|
+
"message": "Starting analysis...",
|
|
711
|
+
"progress": 0,
|
|
712
|
+
},
|
|
713
|
+
)
|
|
714
|
+
|
|
715
|
+
# Run actual analysis via API server
|
|
716
|
+
try:
|
|
717
|
+
self.api_server._run_analysis(session_id)
|
|
718
|
+
|
|
719
|
+
# Send completion message
|
|
720
|
+
await self.ws_manager.send_message(
|
|
721
|
+
session_id,
|
|
722
|
+
{
|
|
723
|
+
"type": "status",
|
|
724
|
+
"status": "complete",
|
|
725
|
+
"message": "Analysis complete",
|
|
726
|
+
"progress": 100,
|
|
727
|
+
},
|
|
728
|
+
)
|
|
729
|
+
except Exception as e:
|
|
730
|
+
# Send error message
|
|
731
|
+
await self.ws_manager.send_message(
|
|
732
|
+
session_id,
|
|
733
|
+
{
|
|
734
|
+
"type": "error",
|
|
735
|
+
"status": "error",
|
|
736
|
+
"message": str(e),
|
|
737
|
+
"progress": 0,
|
|
738
|
+
},
|
|
739
|
+
)
|
|
740
|
+
|
|
741
|
+
def _generate_waveform_data(self, session: dict[str, Any]) -> dict[str, Any]:
|
|
742
|
+
"""Generate waveform data in Plotly.js format.
|
|
743
|
+
|
|
744
|
+
Args:
|
|
745
|
+
session: Session data.
|
|
746
|
+
|
|
747
|
+
Returns:
|
|
748
|
+
Dict with Plotly.js traces.
|
|
749
|
+
"""
|
|
750
|
+
# Simplified waveform generation
|
|
751
|
+
# In production, this would extract actual signal data from the capture
|
|
752
|
+
import numpy as np
|
|
753
|
+
|
|
754
|
+
# Generate sample waveform
|
|
755
|
+
t = np.linspace(0, 1, 1000)
|
|
756
|
+
signal = np.sin(2 * np.pi * 5 * t) + 0.5 * np.sin(2 * np.pi * 15 * t)
|
|
757
|
+
|
|
758
|
+
return {
|
|
759
|
+
"data": [
|
|
760
|
+
{
|
|
761
|
+
"x": t.tolist(),
|
|
762
|
+
"y": signal.tolist(),
|
|
763
|
+
"type": "scatter",
|
|
764
|
+
"mode": "lines",
|
|
765
|
+
"name": "Signal",
|
|
766
|
+
"line": {"color": "#00d9ff", "width": 1},
|
|
767
|
+
}
|
|
768
|
+
],
|
|
769
|
+
"layout": {
|
|
770
|
+
"title": f"Waveform: {session['filename']}",
|
|
771
|
+
"xaxis": {"title": "Time (s)"},
|
|
772
|
+
"yaxis": {"title": "Amplitude"},
|
|
773
|
+
"template": "plotly_dark" if self.config.theme == "dark" else "plotly_white",
|
|
774
|
+
"hovermode": "x unified",
|
|
775
|
+
},
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
def run(self, reload: bool = False) -> None:
|
|
779
|
+
"""Start the web dashboard server.
|
|
780
|
+
|
|
781
|
+
Args:
|
|
782
|
+
reload: Enable auto-reload for development.
|
|
783
|
+
"""
|
|
784
|
+
try:
|
|
785
|
+
import uvicorn
|
|
786
|
+
except ImportError as e:
|
|
787
|
+
raise ImportError("uvicorn required. Install with: pip install uvicorn") from e
|
|
788
|
+
|
|
789
|
+
logger.info(f"Starting Oscura Web Dashboard on {self.host}:{self.port}")
|
|
790
|
+
logger.info(f"Dashboard URL: http://{self.host}:{self.port}")
|
|
791
|
+
|
|
792
|
+
uvicorn.run(
|
|
793
|
+
self.app,
|
|
794
|
+
host=self.host,
|
|
795
|
+
port=self.port,
|
|
796
|
+
reload=reload,
|
|
797
|
+
log_level="info",
|
|
798
|
+
)
|
|
799
|
+
|
|
800
|
+
|
|
801
|
+
# ============================================================================
|
|
802
|
+
# Command-Line Interface
|
|
803
|
+
# ============================================================================
|
|
804
|
+
|
|
805
|
+
|
|
806
|
+
def main() -> None:
|
|
807
|
+
"""Command-line interface for web dashboard."""
|
|
808
|
+
import argparse
|
|
809
|
+
|
|
810
|
+
parser = argparse.ArgumentParser(
|
|
811
|
+
description="Oscura Web Dashboard",
|
|
812
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
813
|
+
)
|
|
814
|
+
parser.add_argument(
|
|
815
|
+
"--host",
|
|
816
|
+
type=str,
|
|
817
|
+
default="127.0.0.1",
|
|
818
|
+
help="Server host address (default: 127.0.0.1)",
|
|
819
|
+
)
|
|
820
|
+
parser.add_argument(
|
|
821
|
+
"--port",
|
|
822
|
+
type=int,
|
|
823
|
+
default=5000,
|
|
824
|
+
help="Server port number (default: 5000)",
|
|
825
|
+
)
|
|
826
|
+
parser.add_argument(
|
|
827
|
+
"--theme",
|
|
828
|
+
type=str,
|
|
829
|
+
choices=["light", "dark"],
|
|
830
|
+
default="dark",
|
|
831
|
+
help="UI theme (default: dark)",
|
|
832
|
+
)
|
|
833
|
+
parser.add_argument(
|
|
834
|
+
"--reload",
|
|
835
|
+
action="store_true",
|
|
836
|
+
help="Enable auto-reload for development",
|
|
837
|
+
)
|
|
838
|
+
|
|
839
|
+
args = parser.parse_args()
|
|
840
|
+
|
|
841
|
+
# Create configuration
|
|
842
|
+
config = DashboardConfig(theme=args.theme)
|
|
843
|
+
|
|
844
|
+
# Create and run dashboard
|
|
845
|
+
dashboard = WebDashboard(host=args.host, port=args.port, config=config)
|
|
846
|
+
dashboard.run(reload=args.reload)
|
|
847
|
+
|
|
848
|
+
|
|
849
|
+
if __name__ == "__main__":
|
|
850
|
+
main()
|