oscura 0.5.0__py3-none-any.whl → 0.6.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- oscura/__init__.py +169 -167
- oscura/analyzers/__init__.py +3 -0
- oscura/analyzers/classification.py +659 -0
- oscura/analyzers/digital/__init__.py +0 -48
- oscura/analyzers/digital/edges.py +325 -65
- oscura/analyzers/digital/extraction.py +0 -195
- oscura/analyzers/digital/quality.py +293 -166
- oscura/analyzers/digital/timing.py +260 -115
- oscura/analyzers/digital/timing_numba.py +334 -0
- oscura/analyzers/entropy.py +605 -0
- oscura/analyzers/eye/diagram.py +176 -109
- oscura/analyzers/eye/metrics.py +5 -5
- oscura/analyzers/jitter/__init__.py +6 -4
- oscura/analyzers/jitter/ber.py +52 -52
- oscura/analyzers/jitter/classification.py +156 -0
- oscura/analyzers/jitter/decomposition.py +163 -113
- oscura/analyzers/jitter/spectrum.py +80 -64
- oscura/analyzers/ml/__init__.py +39 -0
- oscura/analyzers/ml/features.py +600 -0
- oscura/analyzers/ml/signal_classifier.py +604 -0
- oscura/analyzers/packet/daq.py +246 -158
- oscura/analyzers/packet/parser.py +12 -1
- oscura/analyzers/packet/payload.py +50 -2110
- oscura/analyzers/packet/payload_analysis.py +361 -181
- oscura/analyzers/packet/payload_patterns.py +133 -70
- oscura/analyzers/packet/stream.py +84 -23
- oscura/analyzers/patterns/__init__.py +26 -5
- oscura/analyzers/patterns/anomaly_detection.py +908 -0
- oscura/analyzers/patterns/clustering.py +169 -108
- oscura/analyzers/patterns/clustering_optimized.py +227 -0
- oscura/analyzers/patterns/discovery.py +1 -1
- oscura/analyzers/patterns/matching.py +581 -197
- oscura/analyzers/patterns/pattern_mining.py +778 -0
- oscura/analyzers/patterns/periodic.py +121 -38
- oscura/analyzers/patterns/sequences.py +175 -78
- oscura/analyzers/power/conduction.py +1 -1
- oscura/analyzers/power/soa.py +6 -6
- oscura/analyzers/power/switching.py +250 -110
- oscura/analyzers/protocol/__init__.py +17 -1
- oscura/analyzers/protocols/__init__.py +1 -22
- oscura/analyzers/protocols/base.py +6 -6
- oscura/analyzers/protocols/ble/__init__.py +38 -0
- oscura/analyzers/protocols/ble/analyzer.py +809 -0
- oscura/analyzers/protocols/ble/uuids.py +288 -0
- oscura/analyzers/protocols/can.py +257 -127
- oscura/analyzers/protocols/can_fd.py +107 -80
- oscura/analyzers/protocols/flexray.py +139 -80
- oscura/analyzers/protocols/hdlc.py +93 -58
- oscura/analyzers/protocols/i2c.py +247 -106
- oscura/analyzers/protocols/i2s.py +138 -86
- oscura/analyzers/protocols/industrial/__init__.py +40 -0
- oscura/analyzers/protocols/industrial/bacnet/__init__.py +33 -0
- oscura/analyzers/protocols/industrial/bacnet/analyzer.py +708 -0
- oscura/analyzers/protocols/industrial/bacnet/encoding.py +412 -0
- oscura/analyzers/protocols/industrial/bacnet/services.py +622 -0
- oscura/analyzers/protocols/industrial/ethercat/__init__.py +30 -0
- oscura/analyzers/protocols/industrial/ethercat/analyzer.py +474 -0
- oscura/analyzers/protocols/industrial/ethercat/mailbox.py +339 -0
- oscura/analyzers/protocols/industrial/ethercat/topology.py +166 -0
- oscura/analyzers/protocols/industrial/modbus/__init__.py +31 -0
- oscura/analyzers/protocols/industrial/modbus/analyzer.py +525 -0
- oscura/analyzers/protocols/industrial/modbus/crc.py +79 -0
- oscura/analyzers/protocols/industrial/modbus/functions.py +436 -0
- oscura/analyzers/protocols/industrial/opcua/__init__.py +21 -0
- oscura/analyzers/protocols/industrial/opcua/analyzer.py +552 -0
- oscura/analyzers/protocols/industrial/opcua/datatypes.py +446 -0
- oscura/analyzers/protocols/industrial/opcua/services.py +264 -0
- oscura/analyzers/protocols/industrial/profinet/__init__.py +23 -0
- oscura/analyzers/protocols/industrial/profinet/analyzer.py +441 -0
- oscura/analyzers/protocols/industrial/profinet/dcp.py +263 -0
- oscura/analyzers/protocols/industrial/profinet/ptcp.py +200 -0
- oscura/analyzers/protocols/jtag.py +180 -98
- oscura/analyzers/protocols/lin.py +219 -114
- oscura/analyzers/protocols/manchester.py +4 -4
- oscura/analyzers/protocols/onewire.py +253 -149
- oscura/analyzers/protocols/parallel_bus/__init__.py +20 -0
- oscura/analyzers/protocols/parallel_bus/centronics.py +92 -0
- oscura/analyzers/protocols/parallel_bus/gpib.py +137 -0
- oscura/analyzers/protocols/spi.py +192 -95
- oscura/analyzers/protocols/swd.py +321 -167
- oscura/analyzers/protocols/uart.py +267 -125
- oscura/analyzers/protocols/usb.py +235 -131
- oscura/analyzers/side_channel/power.py +17 -12
- oscura/analyzers/signal/__init__.py +15 -0
- oscura/analyzers/signal/timing_analysis.py +1086 -0
- oscura/analyzers/signal_integrity/__init__.py +4 -1
- oscura/analyzers/signal_integrity/sparams.py +2 -19
- oscura/analyzers/spectral/chunked.py +129 -60
- oscura/analyzers/spectral/chunked_fft.py +300 -94
- oscura/analyzers/spectral/chunked_wavelet.py +100 -80
- oscura/analyzers/statistical/checksum.py +376 -217
- oscura/analyzers/statistical/classification.py +229 -107
- oscura/analyzers/statistical/entropy.py +78 -53
- oscura/analyzers/statistics/correlation.py +407 -211
- oscura/analyzers/statistics/outliers.py +2 -2
- oscura/analyzers/statistics/streaming.py +30 -5
- oscura/analyzers/validation.py +216 -101
- oscura/analyzers/waveform/measurements.py +9 -0
- oscura/analyzers/waveform/measurements_with_uncertainty.py +31 -15
- oscura/analyzers/waveform/spectral.py +500 -228
- oscura/api/__init__.py +31 -5
- oscura/api/dsl/__init__.py +582 -0
- oscura/{dsl → api/dsl}/commands.py +43 -76
- oscura/{dsl → api/dsl}/interpreter.py +26 -51
- oscura/{dsl → api/dsl}/parser.py +107 -77
- oscura/{dsl → api/dsl}/repl.py +2 -2
- oscura/api/dsl.py +1 -1
- oscura/{integrations → api/integrations}/__init__.py +1 -1
- oscura/{integrations → api/integrations}/llm.py +201 -102
- oscura/api/operators.py +3 -3
- oscura/api/optimization.py +144 -30
- oscura/api/rest_server.py +921 -0
- oscura/api/server/__init__.py +17 -0
- oscura/api/server/dashboard.py +850 -0
- oscura/api/server/static/README.md +34 -0
- oscura/api/server/templates/base.html +181 -0
- oscura/api/server/templates/export.html +120 -0
- oscura/api/server/templates/home.html +284 -0
- oscura/api/server/templates/protocols.html +58 -0
- oscura/api/server/templates/reports.html +43 -0
- oscura/api/server/templates/session_detail.html +89 -0
- oscura/api/server/templates/sessions.html +83 -0
- oscura/api/server/templates/waveforms.html +73 -0
- oscura/automotive/__init__.py +8 -1
- oscura/automotive/can/__init__.py +10 -0
- oscura/automotive/can/checksum.py +3 -1
- oscura/automotive/can/dbc_generator.py +590 -0
- oscura/automotive/can/message_wrapper.py +121 -74
- oscura/automotive/can/patterns.py +98 -21
- oscura/automotive/can/session.py +292 -56
- oscura/automotive/can/state_machine.py +6 -3
- oscura/automotive/can/stimulus_response.py +97 -75
- oscura/automotive/dbc/__init__.py +10 -2
- oscura/automotive/dbc/generator.py +84 -56
- oscura/automotive/dbc/parser.py +6 -6
- oscura/automotive/dtc/data.json +2763 -0
- oscura/automotive/dtc/database.py +2 -2
- oscura/automotive/flexray/__init__.py +31 -0
- oscura/automotive/flexray/analyzer.py +504 -0
- oscura/automotive/flexray/crc.py +185 -0
- oscura/automotive/flexray/fibex.py +449 -0
- oscura/automotive/j1939/__init__.py +45 -8
- oscura/automotive/j1939/analyzer.py +605 -0
- oscura/automotive/j1939/spns.py +326 -0
- oscura/automotive/j1939/transport.py +306 -0
- oscura/automotive/lin/__init__.py +47 -0
- oscura/automotive/lin/analyzer.py +612 -0
- oscura/automotive/loaders/blf.py +13 -2
- oscura/automotive/loaders/csv_can.py +143 -72
- oscura/automotive/loaders/dispatcher.py +50 -2
- oscura/automotive/loaders/mdf.py +86 -45
- oscura/automotive/loaders/pcap.py +111 -61
- oscura/automotive/uds/__init__.py +4 -0
- oscura/automotive/uds/analyzer.py +725 -0
- oscura/automotive/uds/decoder.py +140 -58
- oscura/automotive/uds/models.py +7 -1
- oscura/automotive/visualization.py +1 -1
- oscura/cli/analyze.py +348 -0
- oscura/cli/batch.py +142 -122
- oscura/cli/benchmark.py +275 -0
- oscura/cli/characterize.py +137 -82
- oscura/cli/compare.py +224 -131
- oscura/cli/completion.py +250 -0
- oscura/cli/config_cmd.py +361 -0
- oscura/cli/decode.py +164 -87
- oscura/cli/export.py +286 -0
- oscura/cli/main.py +115 -31
- oscura/{onboarding → cli/onboarding}/__init__.py +3 -3
- oscura/{onboarding → cli/onboarding}/help.py +80 -58
- oscura/{onboarding → cli/onboarding}/tutorials.py +97 -72
- oscura/{onboarding → cli/onboarding}/wizard.py +55 -36
- oscura/cli/progress.py +147 -0
- oscura/cli/shell.py +157 -135
- oscura/cli/validate_cmd.py +204 -0
- oscura/cli/visualize.py +158 -0
- oscura/convenience.py +125 -79
- oscura/core/__init__.py +4 -2
- oscura/core/backend_selector.py +3 -3
- oscura/core/cache.py +126 -15
- oscura/core/cancellation.py +1 -1
- oscura/{config → core/config}/__init__.py +20 -11
- oscura/{config → core/config}/defaults.py +1 -1
- oscura/{config → core/config}/loader.py +7 -5
- oscura/{config → core/config}/memory.py +5 -5
- oscura/{config → core/config}/migration.py +1 -1
- oscura/{config → core/config}/pipeline.py +99 -23
- oscura/{config → core/config}/preferences.py +1 -1
- oscura/{config → core/config}/protocol.py +3 -3
- oscura/{config → core/config}/schema.py +426 -272
- oscura/{config → core/config}/settings.py +1 -1
- oscura/{config → core/config}/thresholds.py +195 -153
- oscura/core/correlation.py +5 -6
- oscura/core/cross_domain.py +0 -2
- oscura/core/debug.py +9 -5
- oscura/{extensibility → core/extensibility}/docs.py +158 -70
- oscura/{extensibility → core/extensibility}/extensions.py +160 -76
- oscura/{extensibility → core/extensibility}/logging.py +1 -1
- oscura/{extensibility → core/extensibility}/measurements.py +1 -1
- oscura/{extensibility → core/extensibility}/plugins.py +1 -1
- oscura/{extensibility → core/extensibility}/templates.py +73 -3
- oscura/{extensibility → core/extensibility}/validation.py +1 -1
- oscura/core/gpu_backend.py +11 -7
- oscura/core/log_query.py +101 -11
- oscura/core/logging.py +126 -54
- oscura/core/logging_advanced.py +5 -5
- oscura/core/memory_limits.py +108 -70
- oscura/core/memory_monitor.py +2 -2
- oscura/core/memory_progress.py +7 -7
- oscura/core/memory_warnings.py +1 -1
- oscura/core/numba_backend.py +13 -13
- oscura/{plugins → core/plugins}/__init__.py +9 -9
- oscura/{plugins → core/plugins}/base.py +7 -7
- oscura/{plugins → core/plugins}/cli.py +3 -3
- oscura/{plugins → core/plugins}/discovery.py +186 -106
- oscura/{plugins → core/plugins}/lifecycle.py +1 -1
- oscura/{plugins → core/plugins}/manager.py +7 -7
- oscura/{plugins → core/plugins}/registry.py +3 -3
- oscura/{plugins → core/plugins}/versioning.py +1 -1
- oscura/core/progress.py +16 -1
- oscura/core/provenance.py +8 -2
- oscura/{schemas → core/schemas}/__init__.py +2 -2
- oscura/core/schemas/bus_configuration.json +322 -0
- oscura/core/schemas/device_mapping.json +182 -0
- oscura/core/schemas/packet_format.json +418 -0
- oscura/core/schemas/protocol_definition.json +363 -0
- oscura/core/types.py +4 -0
- oscura/core/uncertainty.py +3 -3
- oscura/correlation/__init__.py +52 -0
- oscura/correlation/multi_protocol.py +811 -0
- oscura/discovery/auto_decoder.py +117 -35
- oscura/discovery/comparison.py +191 -86
- oscura/discovery/quality_validator.py +155 -68
- oscura/discovery/signal_detector.py +196 -79
- oscura/export/__init__.py +18 -20
- oscura/export/kaitai_struct.py +513 -0
- oscura/export/scapy_layer.py +801 -0
- oscura/export/wireshark/README.md +15 -15
- oscura/export/wireshark/generator.py +1 -1
- oscura/export/wireshark/templates/dissector.lua.j2 +2 -2
- oscura/export/wireshark_dissector.py +746 -0
- oscura/guidance/wizard.py +207 -111
- oscura/hardware/__init__.py +19 -0
- oscura/{acquisition → hardware/acquisition}/__init__.py +4 -4
- oscura/{acquisition → hardware/acquisition}/file.py +2 -2
- oscura/{acquisition → hardware/acquisition}/hardware.py +7 -7
- oscura/{acquisition → hardware/acquisition}/saleae.py +15 -12
- oscura/{acquisition → hardware/acquisition}/socketcan.py +1 -1
- oscura/{acquisition → hardware/acquisition}/streaming.py +2 -2
- oscura/{acquisition → hardware/acquisition}/synthetic.py +3 -3
- oscura/{acquisition → hardware/acquisition}/visa.py +33 -11
- oscura/hardware/firmware/__init__.py +29 -0
- oscura/hardware/firmware/pattern_recognition.py +874 -0
- oscura/hardware/hal_detector.py +736 -0
- oscura/hardware/security/__init__.py +37 -0
- oscura/hardware/security/side_channel_detector.py +1126 -0
- oscura/inference/__init__.py +4 -0
- oscura/inference/active_learning/README.md +7 -7
- oscura/inference/active_learning/observation_table.py +4 -1
- oscura/inference/alignment.py +216 -123
- oscura/inference/bayesian.py +113 -33
- oscura/inference/crc_reverse.py +101 -55
- oscura/inference/logic.py +6 -2
- oscura/inference/message_format.py +342 -183
- oscura/inference/protocol.py +95 -44
- oscura/inference/protocol_dsl.py +180 -82
- oscura/inference/signal_intelligence.py +1439 -706
- oscura/inference/spectral.py +99 -57
- oscura/inference/state_machine.py +810 -158
- oscura/inference/stream.py +270 -110
- oscura/iot/__init__.py +34 -0
- oscura/iot/coap/__init__.py +32 -0
- oscura/iot/coap/analyzer.py +668 -0
- oscura/iot/coap/options.py +212 -0
- oscura/iot/lorawan/__init__.py +21 -0
- oscura/iot/lorawan/crypto.py +206 -0
- oscura/iot/lorawan/decoder.py +801 -0
- oscura/iot/lorawan/mac_commands.py +341 -0
- oscura/iot/mqtt/__init__.py +27 -0
- oscura/iot/mqtt/analyzer.py +999 -0
- oscura/iot/mqtt/properties.py +315 -0
- oscura/iot/zigbee/__init__.py +31 -0
- oscura/iot/zigbee/analyzer.py +615 -0
- oscura/iot/zigbee/security.py +153 -0
- oscura/iot/zigbee/zcl.py +349 -0
- oscura/jupyter/display.py +125 -45
- oscura/{exploratory → jupyter/exploratory}/__init__.py +8 -8
- oscura/{exploratory → jupyter/exploratory}/error_recovery.py +298 -141
- oscura/jupyter/exploratory/fuzzy.py +746 -0
- oscura/{exploratory → jupyter/exploratory}/fuzzy_advanced.py +258 -100
- oscura/{exploratory → jupyter/exploratory}/legacy.py +464 -242
- oscura/{exploratory → jupyter/exploratory}/parse.py +167 -145
- oscura/{exploratory → jupyter/exploratory}/recovery.py +119 -87
- oscura/jupyter/exploratory/sync.py +612 -0
- oscura/{exploratory → jupyter/exploratory}/unknown.py +299 -176
- oscura/jupyter/magic.py +4 -4
- oscura/{ui → jupyter/ui}/__init__.py +2 -2
- oscura/{ui → jupyter/ui}/formatters.py +3 -3
- oscura/{ui → jupyter/ui}/progressive_display.py +153 -82
- oscura/loaders/__init__.py +171 -63
- oscura/loaders/binary.py +88 -1
- oscura/loaders/chipwhisperer.py +153 -137
- oscura/loaders/configurable.py +208 -86
- oscura/loaders/csv_loader.py +458 -215
- oscura/loaders/hdf5_loader.py +278 -119
- oscura/loaders/lazy.py +87 -54
- oscura/loaders/mmap_loader.py +1 -1
- oscura/loaders/numpy_loader.py +253 -116
- oscura/loaders/pcap.py +226 -151
- oscura/loaders/rigol.py +110 -49
- oscura/loaders/sigrok.py +201 -78
- oscura/loaders/tdms.py +81 -58
- oscura/loaders/tektronix.py +291 -174
- oscura/loaders/touchstone.py +182 -87
- oscura/loaders/vcd.py +215 -117
- oscura/loaders/wav.py +155 -68
- oscura/reporting/__init__.py +9 -7
- oscura/reporting/analyze.py +352 -146
- oscura/reporting/argument_preparer.py +69 -14
- oscura/reporting/auto_report.py +97 -61
- oscura/reporting/batch.py +131 -58
- oscura/reporting/chart_selection.py +57 -45
- oscura/reporting/comparison.py +63 -17
- oscura/reporting/content/executive.py +76 -24
- oscura/reporting/core_formats/multi_format.py +11 -8
- oscura/reporting/engine.py +312 -158
- oscura/reporting/enhanced_reports.py +949 -0
- oscura/reporting/export.py +86 -43
- oscura/reporting/formatting/numbers.py +69 -42
- oscura/reporting/html.py +139 -58
- oscura/reporting/index.py +137 -65
- oscura/reporting/output.py +158 -67
- oscura/reporting/pdf.py +67 -102
- oscura/reporting/plots.py +191 -112
- oscura/reporting/sections.py +88 -47
- oscura/reporting/standards.py +104 -61
- oscura/reporting/summary_generator.py +75 -55
- oscura/reporting/tables.py +138 -54
- oscura/reporting/templates/enhanced/protocol_re.html +525 -0
- oscura/reporting/templates/index.md +13 -13
- oscura/sessions/__init__.py +14 -23
- oscura/sessions/base.py +3 -3
- oscura/sessions/blackbox.py +106 -10
- oscura/sessions/generic.py +2 -2
- oscura/sessions/legacy.py +783 -0
- oscura/side_channel/__init__.py +63 -0
- oscura/side_channel/dpa.py +1025 -0
- oscura/utils/__init__.py +15 -1
- oscura/utils/autodetect.py +1 -5
- oscura/utils/bitwise.py +118 -0
- oscura/{builders → utils/builders}/__init__.py +1 -1
- oscura/{comparison → utils/comparison}/__init__.py +6 -6
- oscura/{comparison → utils/comparison}/compare.py +202 -101
- oscura/{comparison → utils/comparison}/golden.py +83 -63
- oscura/{comparison → utils/comparison}/limits.py +313 -89
- oscura/{comparison → utils/comparison}/mask.py +151 -45
- oscura/{comparison → utils/comparison}/trace_diff.py +1 -1
- oscura/{comparison → utils/comparison}/visualization.py +147 -89
- oscura/{component → utils/component}/__init__.py +3 -3
- oscura/{component → utils/component}/impedance.py +122 -58
- oscura/{component → utils/component}/reactive.py +165 -168
- oscura/{component → utils/component}/transmission_line.py +3 -3
- oscura/{filtering → utils/filtering}/__init__.py +6 -6
- oscura/{filtering → utils/filtering}/base.py +1 -1
- oscura/{filtering → utils/filtering}/convenience.py +2 -2
- oscura/{filtering → utils/filtering}/design.py +169 -93
- oscura/{filtering → utils/filtering}/filters.py +2 -2
- oscura/{filtering → utils/filtering}/introspection.py +2 -2
- oscura/utils/geometry.py +31 -0
- oscura/utils/imports.py +184 -0
- oscura/utils/lazy.py +1 -1
- oscura/{math → utils/math}/__init__.py +2 -2
- oscura/{math → utils/math}/arithmetic.py +114 -48
- oscura/{math → utils/math}/interpolation.py +139 -106
- oscura/utils/memory.py +129 -66
- oscura/utils/memory_advanced.py +92 -9
- oscura/utils/memory_extensions.py +10 -8
- oscura/{optimization → utils/optimization}/__init__.py +1 -1
- oscura/{optimization → utils/optimization}/search.py +2 -2
- oscura/utils/performance/__init__.py +58 -0
- oscura/utils/performance/caching.py +889 -0
- oscura/utils/performance/lsh_clustering.py +333 -0
- oscura/utils/performance/memory_optimizer.py +699 -0
- oscura/utils/performance/optimizations.py +675 -0
- oscura/utils/performance/parallel.py +654 -0
- oscura/utils/performance/profiling.py +661 -0
- oscura/{pipeline → utils/pipeline}/base.py +1 -1
- oscura/{pipeline → utils/pipeline}/composition.py +11 -3
- oscura/{pipeline → utils/pipeline}/parallel.py +3 -2
- oscura/{pipeline → utils/pipeline}/pipeline.py +1 -1
- oscura/{pipeline → utils/pipeline}/reverse_engineering.py +412 -221
- oscura/{search → utils/search}/__init__.py +3 -3
- oscura/{search → utils/search}/anomaly.py +188 -58
- oscura/utils/search/context.py +294 -0
- oscura/{search → utils/search}/pattern.py +138 -10
- oscura/utils/serial.py +51 -0
- oscura/utils/storage/__init__.py +61 -0
- oscura/utils/storage/database.py +1166 -0
- oscura/{streaming → utils/streaming}/chunked.py +302 -143
- oscura/{streaming → utils/streaming}/progressive.py +1 -1
- oscura/{streaming → utils/streaming}/realtime.py +3 -2
- oscura/{triggering → utils/triggering}/__init__.py +6 -6
- oscura/{triggering → utils/triggering}/base.py +6 -6
- oscura/{triggering → utils/triggering}/edge.py +2 -2
- oscura/{triggering → utils/triggering}/pattern.py +2 -2
- oscura/{triggering → utils/triggering}/pulse.py +115 -74
- oscura/{triggering → utils/triggering}/window.py +2 -2
- oscura/utils/validation.py +32 -0
- oscura/validation/__init__.py +121 -0
- oscura/{compliance → validation/compliance}/__init__.py +5 -5
- oscura/{compliance → validation/compliance}/advanced.py +5 -5
- oscura/{compliance → validation/compliance}/masks.py +1 -1
- oscura/{compliance → validation/compliance}/reporting.py +127 -53
- oscura/{compliance → validation/compliance}/testing.py +114 -52
- oscura/validation/compliance_tests.py +915 -0
- oscura/validation/fuzzer.py +990 -0
- oscura/validation/grammar_tests.py +596 -0
- oscura/validation/grammar_validator.py +904 -0
- oscura/validation/hil_testing.py +977 -0
- oscura/{quality → validation/quality}/__init__.py +4 -4
- oscura/{quality → validation/quality}/ensemble.py +251 -171
- oscura/{quality → validation/quality}/explainer.py +3 -3
- oscura/{quality → validation/quality}/scoring.py +1 -1
- oscura/{quality → validation/quality}/warnings.py +4 -4
- oscura/validation/regression_suite.py +808 -0
- oscura/validation/replay.py +788 -0
- oscura/{testing → validation/testing}/__init__.py +2 -2
- oscura/{testing → validation/testing}/synthetic.py +5 -5
- oscura/visualization/__init__.py +9 -0
- oscura/visualization/accessibility.py +1 -1
- oscura/visualization/annotations.py +64 -67
- oscura/visualization/colors.py +7 -7
- oscura/visualization/digital.py +180 -81
- oscura/visualization/eye.py +236 -85
- oscura/visualization/interactive.py +320 -143
- oscura/visualization/jitter.py +587 -247
- oscura/visualization/layout.py +169 -134
- oscura/visualization/optimization.py +103 -52
- oscura/visualization/palettes.py +1 -1
- oscura/visualization/power.py +427 -211
- oscura/visualization/power_extended.py +626 -297
- oscura/visualization/presets.py +2 -0
- oscura/visualization/protocols.py +495 -181
- oscura/visualization/render.py +79 -63
- oscura/visualization/reverse_engineering.py +171 -124
- oscura/visualization/signal_integrity.py +460 -279
- oscura/visualization/specialized.py +190 -100
- oscura/visualization/spectral.py +670 -255
- oscura/visualization/thumbnails.py +166 -137
- oscura/visualization/waveform.py +150 -63
- oscura/workflows/__init__.py +3 -0
- oscura/{batch → workflows/batch}/__init__.py +5 -5
- oscura/{batch → workflows/batch}/advanced.py +150 -75
- oscura/workflows/batch/aggregate.py +531 -0
- oscura/workflows/batch/analyze.py +236 -0
- oscura/{batch → workflows/batch}/logging.py +2 -2
- oscura/{batch → workflows/batch}/metrics.py +1 -1
- oscura/workflows/complete_re.py +1144 -0
- oscura/workflows/compliance.py +44 -54
- oscura/workflows/digital.py +197 -51
- oscura/workflows/legacy/__init__.py +12 -0
- oscura/{workflow → workflows/legacy}/dag.py +4 -1
- oscura/workflows/multi_trace.py +9 -9
- oscura/workflows/power.py +42 -62
- oscura/workflows/protocol.py +82 -49
- oscura/workflows/reverse_engineering.py +351 -150
- oscura/workflows/signal_integrity.py +157 -82
- oscura-0.6.0.dist-info/METADATA +643 -0
- oscura-0.6.0.dist-info/RECORD +590 -0
- oscura/analyzers/digital/ic_database.py +0 -498
- oscura/analyzers/digital/timing_paths.py +0 -339
- oscura/analyzers/digital/vintage.py +0 -377
- oscura/analyzers/digital/vintage_result.py +0 -148
- oscura/analyzers/protocols/parallel_bus.py +0 -449
- oscura/batch/aggregate.py +0 -300
- oscura/batch/analyze.py +0 -139
- oscura/dsl/__init__.py +0 -73
- oscura/exceptions.py +0 -59
- oscura/exploratory/fuzzy.py +0 -513
- oscura/exploratory/sync.py +0 -384
- oscura/export/wavedrom.py +0 -430
- oscura/exporters/__init__.py +0 -94
- oscura/exporters/csv.py +0 -303
- oscura/exporters/exporters.py +0 -44
- oscura/exporters/hdf5.py +0 -217
- oscura/exporters/html_export.py +0 -701
- oscura/exporters/json_export.py +0 -338
- oscura/exporters/markdown_export.py +0 -367
- oscura/exporters/matlab_export.py +0 -354
- oscura/exporters/npz_export.py +0 -219
- oscura/exporters/spice_export.py +0 -210
- oscura/exporters/vintage_logic_csv.py +0 -247
- oscura/reporting/vintage_logic_report.py +0 -523
- oscura/search/context.py +0 -149
- oscura/session/__init__.py +0 -34
- oscura/session/annotations.py +0 -289
- oscura/session/history.py +0 -313
- oscura/session/session.py +0 -520
- oscura/visualization/digital_advanced.py +0 -718
- oscura/visualization/figure_manager.py +0 -156
- oscura/workflow/__init__.py +0 -13
- oscura-0.5.0.dist-info/METADATA +0 -407
- oscura-0.5.0.dist-info/RECORD +0 -486
- /oscura/core/{config.py → config/legacy.py} +0 -0
- /oscura/{extensibility → core/extensibility}/__init__.py +0 -0
- /oscura/{extensibility → core/extensibility}/registry.py +0 -0
- /oscura/{plugins → core/plugins}/isolation.py +0 -0
- /oscura/{builders → utils/builders}/signal_builder.py +0 -0
- /oscura/{optimization → utils/optimization}/parallel.py +0 -0
- /oscura/{pipeline → utils/pipeline}/__init__.py +0 -0
- /oscura/{streaming → utils/streaming}/__init__.py +0 -0
- {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/WHEEL +0 -0
- {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/entry_points.txt +0 -0
- {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,921 @@
|
|
|
1
|
+
"""REST API server for Oscura protocol analysis.
|
|
2
|
+
|
|
3
|
+
This module provides a RESTful API interface for accessing Oscura functionality
|
|
4
|
+
remotely, enabling web dashboards, automation, and integration with other tools.
|
|
5
|
+
|
|
6
|
+
Supports:
|
|
7
|
+
- File upload and analysis
|
|
8
|
+
- Session management
|
|
9
|
+
- Protocol discovery
|
|
10
|
+
- Export to multiple formats (Wireshark, Scapy, Kaitai)
|
|
11
|
+
- OpenAPI documentation
|
|
12
|
+
|
|
13
|
+
Example:
|
|
14
|
+
>>> from oscura.api.rest_server import RESTAPIServer
|
|
15
|
+
>>> server = RESTAPIServer(host="0.0.0.0", port=8000)
|
|
16
|
+
>>> server.run() # Starts server on http://0.0.0.0:8000
|
|
17
|
+
>>> # API docs at http://0.0.0.0:8000/docs
|
|
18
|
+
|
|
19
|
+
Architecture:
|
|
20
|
+
- FastAPI framework (with Flask fallback if unavailable)
|
|
21
|
+
- Async request processing for large files
|
|
22
|
+
- CORS support for web clients
|
|
23
|
+
- Rate limiting for API protection
|
|
24
|
+
- Authentication support (API keys)
|
|
25
|
+
- OpenAPI/Swagger documentation
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import hashlib
|
|
31
|
+
import logging
|
|
32
|
+
import tempfile
|
|
33
|
+
import time
|
|
34
|
+
import uuid
|
|
35
|
+
from dataclasses import dataclass, field
|
|
36
|
+
from datetime import datetime
|
|
37
|
+
from pathlib import Path
|
|
38
|
+
from typing import TYPE_CHECKING, Any
|
|
39
|
+
|
|
40
|
+
if TYPE_CHECKING:
|
|
41
|
+
from oscura.workflows.complete_re import CompleteREResult
|
|
42
|
+
|
|
43
|
+
logger = logging.getLogger(__name__)
|
|
44
|
+
|
|
45
|
+
# Try to import FastAPI, fallback to Flask if unavailable
|
|
46
|
+
try:
|
|
47
|
+
from fastapi import (
|
|
48
|
+
BackgroundTasks,
|
|
49
|
+
Depends,
|
|
50
|
+
FastAPI,
|
|
51
|
+
HTTPException,
|
|
52
|
+
Security,
|
|
53
|
+
UploadFile,
|
|
54
|
+
status,
|
|
55
|
+
)
|
|
56
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
57
|
+
from fastapi.security import (
|
|
58
|
+
HTTPAuthorizationCredentials,
|
|
59
|
+
HTTPBearer,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
HAS_FASTAPI = True
|
|
63
|
+
except ImportError:
|
|
64
|
+
HAS_FASTAPI = False
|
|
65
|
+
logger.warning("FastAPI not available. Install with: pip install 'fastapi[all]' uvicorn")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# ============================================================================
|
|
69
|
+
# Request/Response Models
|
|
70
|
+
# ============================================================================
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass
|
|
74
|
+
class AnalysisRequest:
|
|
75
|
+
"""Request model for protocol analysis.
|
|
76
|
+
|
|
77
|
+
Attributes:
|
|
78
|
+
file_data: Uploaded file bytes.
|
|
79
|
+
filename: Original filename.
|
|
80
|
+
protocol_hint: Optional protocol type hint (uart, spi, i2c, can).
|
|
81
|
+
auto_crc: Enable automatic CRC recovery.
|
|
82
|
+
detect_crypto: Enable cryptographic field detection.
|
|
83
|
+
generate_tests: Generate test vectors.
|
|
84
|
+
export_formats: Formats to export (wireshark, scapy, kaitai).
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
file_data: bytes
|
|
88
|
+
filename: str
|
|
89
|
+
protocol_hint: str | None = None
|
|
90
|
+
auto_crc: bool = True
|
|
91
|
+
detect_crypto: bool = True
|
|
92
|
+
generate_tests: bool = True
|
|
93
|
+
export_formats: list[str] = field(default_factory=lambda: ["wireshark"])
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@dataclass
|
|
97
|
+
class AnalysisResponse:
|
|
98
|
+
"""Response model for analysis request.
|
|
99
|
+
|
|
100
|
+
Attributes:
|
|
101
|
+
session_id: Unique session identifier.
|
|
102
|
+
status: Analysis status (processing, complete, error).
|
|
103
|
+
protocols_found: List of detected protocols.
|
|
104
|
+
confidence_scores: Confidence scores per protocol (0.0-1.0).
|
|
105
|
+
message: Human-readable status message.
|
|
106
|
+
created_at: Timestamp of analysis start.
|
|
107
|
+
estimated_duration: Estimated completion time (seconds).
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
session_id: str
|
|
111
|
+
status: str
|
|
112
|
+
protocols_found: list[str] = field(default_factory=list)
|
|
113
|
+
confidence_scores: dict[str, float] = field(default_factory=dict)
|
|
114
|
+
message: str = ""
|
|
115
|
+
created_at: str = ""
|
|
116
|
+
estimated_duration: float = 0.0
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@dataclass
|
|
120
|
+
class SessionResponse:
|
|
121
|
+
"""Response model for session details.
|
|
122
|
+
|
|
123
|
+
Attributes:
|
|
124
|
+
session_id: Unique session identifier.
|
|
125
|
+
status: Current session status.
|
|
126
|
+
protocol_spec: Inferred protocol specification.
|
|
127
|
+
messages_decoded: Number of messages decoded.
|
|
128
|
+
fields_discovered: Number of fields discovered.
|
|
129
|
+
artifacts: Dict of generated artifacts (paths).
|
|
130
|
+
statistics: Analysis statistics.
|
|
131
|
+
created_at: Session creation timestamp.
|
|
132
|
+
updated_at: Last update timestamp.
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
session_id: str
|
|
136
|
+
status: str
|
|
137
|
+
protocol_spec: dict[str, Any] | None = None
|
|
138
|
+
messages_decoded: int = 0
|
|
139
|
+
fields_discovered: int = 0
|
|
140
|
+
artifacts: dict[str, str] = field(default_factory=dict)
|
|
141
|
+
statistics: dict[str, Any] = field(default_factory=dict)
|
|
142
|
+
created_at: str = ""
|
|
143
|
+
updated_at: str = ""
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@dataclass
|
|
147
|
+
class ProtocolResponse:
|
|
148
|
+
"""Response model for protocol details.
|
|
149
|
+
|
|
150
|
+
Attributes:
|
|
151
|
+
protocol_name: Detected protocol name.
|
|
152
|
+
confidence: Detection confidence (0.0-1.0).
|
|
153
|
+
message_count: Number of messages.
|
|
154
|
+
field_count: Number of fields.
|
|
155
|
+
fields: List of field specifications.
|
|
156
|
+
state_machine: State machine if extracted.
|
|
157
|
+
crc_info: CRC parameters if recovered.
|
|
158
|
+
"""
|
|
159
|
+
|
|
160
|
+
protocol_name: str
|
|
161
|
+
confidence: float
|
|
162
|
+
message_count: int
|
|
163
|
+
field_count: int
|
|
164
|
+
fields: list[dict[str, Any]] = field(default_factory=list)
|
|
165
|
+
state_machine: dict[str, Any] | None = None
|
|
166
|
+
crc_info: dict[str, Any] | None = None
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@dataclass
|
|
170
|
+
class ErrorResponse:
|
|
171
|
+
"""Response model for errors.
|
|
172
|
+
|
|
173
|
+
Attributes:
|
|
174
|
+
error_code: Error code identifier.
|
|
175
|
+
message: Human-readable error message.
|
|
176
|
+
details: Additional error details.
|
|
177
|
+
timestamp: Error timestamp.
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
error_code: str
|
|
181
|
+
message: str
|
|
182
|
+
details: dict[str, Any] = field(default_factory=dict)
|
|
183
|
+
timestamp: str = field(default_factory=lambda: datetime.utcnow().isoformat())
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
# ============================================================================
|
|
187
|
+
# Session Management
|
|
188
|
+
# ============================================================================
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class SessionManager:
|
|
192
|
+
"""Manages active analysis sessions.
|
|
193
|
+
|
|
194
|
+
Attributes:
|
|
195
|
+
sessions: Dict of session_id -> session data.
|
|
196
|
+
max_sessions: Maximum concurrent sessions.
|
|
197
|
+
session_timeout: Session timeout in seconds (default 1 hour).
|
|
198
|
+
"""
|
|
199
|
+
|
|
200
|
+
def __init__(self, max_sessions: int = 100, session_timeout: float = 3600.0):
|
|
201
|
+
"""Initialize session manager.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
max_sessions: Maximum concurrent sessions.
|
|
205
|
+
session_timeout: Session timeout in seconds.
|
|
206
|
+
"""
|
|
207
|
+
self.sessions: dict[str, dict[str, Any]] = {}
|
|
208
|
+
self.max_sessions = max_sessions
|
|
209
|
+
self.session_timeout = session_timeout
|
|
210
|
+
|
|
211
|
+
def create_session(self, filename: str, file_data: bytes, options: dict[str, Any]) -> str:
|
|
212
|
+
"""Create a new analysis session.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
filename: Uploaded filename.
|
|
216
|
+
file_data: File content bytes.
|
|
217
|
+
options: Analysis options.
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
Unique session ID.
|
|
221
|
+
|
|
222
|
+
Raises:
|
|
223
|
+
RuntimeError: If max sessions exceeded.
|
|
224
|
+
"""
|
|
225
|
+
if len(self.sessions) >= self.max_sessions:
|
|
226
|
+
# Clean up old sessions
|
|
227
|
+
self._cleanup_old_sessions()
|
|
228
|
+
|
|
229
|
+
if len(self.sessions) >= self.max_sessions:
|
|
230
|
+
raise RuntimeError(f"Maximum sessions ({self.max_sessions}) exceeded")
|
|
231
|
+
|
|
232
|
+
session_id = str(uuid.uuid4())
|
|
233
|
+
self.sessions[session_id] = {
|
|
234
|
+
"id": session_id,
|
|
235
|
+
"filename": filename,
|
|
236
|
+
"file_data": file_data,
|
|
237
|
+
"file_hash": hashlib.sha256(file_data).hexdigest(),
|
|
238
|
+
"options": options,
|
|
239
|
+
"status": "created",
|
|
240
|
+
"result": None,
|
|
241
|
+
"error": None,
|
|
242
|
+
"created_at": datetime.utcnow().isoformat(),
|
|
243
|
+
"updated_at": datetime.utcnow().isoformat(),
|
|
244
|
+
"accessed_at": time.time(),
|
|
245
|
+
}
|
|
246
|
+
return session_id
|
|
247
|
+
|
|
248
|
+
def get_session(self, session_id: str) -> dict[str, Any] | None:
|
|
249
|
+
"""Get session by ID.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
session_id: Session identifier.
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
Session data or None if not found.
|
|
256
|
+
"""
|
|
257
|
+
if session_id in self.sessions:
|
|
258
|
+
self.sessions[session_id]["accessed_at"] = time.time()
|
|
259
|
+
return self.sessions[session_id]
|
|
260
|
+
return None
|
|
261
|
+
|
|
262
|
+
def update_session(
|
|
263
|
+
self, session_id: str, status: str, result: Any = None, error: str | None = None
|
|
264
|
+
) -> None:
|
|
265
|
+
"""Update session status.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
session_id: Session identifier.
|
|
269
|
+
status: New status.
|
|
270
|
+
result: Analysis result.
|
|
271
|
+
error: Error message if failed.
|
|
272
|
+
"""
|
|
273
|
+
if session_id in self.sessions:
|
|
274
|
+
self.sessions[session_id]["status"] = status
|
|
275
|
+
self.sessions[session_id]["result"] = result
|
|
276
|
+
self.sessions[session_id]["error"] = error
|
|
277
|
+
self.sessions[session_id]["updated_at"] = datetime.utcnow().isoformat()
|
|
278
|
+
self.sessions[session_id]["accessed_at"] = time.time()
|
|
279
|
+
|
|
280
|
+
def delete_session(self, session_id: str) -> bool:
|
|
281
|
+
"""Delete a session.
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
session_id: Session identifier.
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
True if deleted, False if not found.
|
|
288
|
+
"""
|
|
289
|
+
if session_id in self.sessions:
|
|
290
|
+
del self.sessions[session_id]
|
|
291
|
+
return True
|
|
292
|
+
return False
|
|
293
|
+
|
|
294
|
+
def list_sessions(self) -> list[dict[str, Any]]:
|
|
295
|
+
"""List all active sessions.
|
|
296
|
+
|
|
297
|
+
Returns:
|
|
298
|
+
List of session summaries.
|
|
299
|
+
"""
|
|
300
|
+
return [
|
|
301
|
+
{
|
|
302
|
+
"session_id": sid,
|
|
303
|
+
"status": data["status"],
|
|
304
|
+
"filename": data["filename"],
|
|
305
|
+
"created_at": data["created_at"],
|
|
306
|
+
"updated_at": data["updated_at"],
|
|
307
|
+
}
|
|
308
|
+
for sid, data in self.sessions.items()
|
|
309
|
+
]
|
|
310
|
+
|
|
311
|
+
def _cleanup_old_sessions(self) -> None:
|
|
312
|
+
"""Remove sessions that have exceeded timeout."""
|
|
313
|
+
current_time = time.time()
|
|
314
|
+
to_delete = [
|
|
315
|
+
sid
|
|
316
|
+
for sid, data in self.sessions.items()
|
|
317
|
+
if current_time - data["accessed_at"] > self.session_timeout
|
|
318
|
+
]
|
|
319
|
+
for sid in to_delete:
|
|
320
|
+
logger.info(f"Cleaning up timed-out session: {sid}")
|
|
321
|
+
self.delete_session(sid)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
# ============================================================================
|
|
325
|
+
# REST API Server (FastAPI)
|
|
326
|
+
# ============================================================================
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
class RESTAPIServer:
|
|
330
|
+
"""REST API server for Oscura.
|
|
331
|
+
|
|
332
|
+
Provides HTTP endpoints for protocol analysis, session management,
|
|
333
|
+
and artifact export.
|
|
334
|
+
|
|
335
|
+
Security Warning:
|
|
336
|
+
Default CORS configuration allows all origins (["*"]) for development
|
|
337
|
+
convenience. For production deployments, explicitly configure allowed
|
|
338
|
+
origins to prevent CSRF attacks:
|
|
339
|
+
|
|
340
|
+
Example (Production):
|
|
341
|
+
server = RESTAPIServer(
|
|
342
|
+
api_key="your-secret-key", # Always set in production
|
|
343
|
+
enable_cors=True,
|
|
344
|
+
cors_origins=["https://trusted-domain.com"]
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
Never deploy to production with:
|
|
348
|
+
- cors_origins=["*"]
|
|
349
|
+
- No api_key configured
|
|
350
|
+
- Exposed to public internet without reverse proxy
|
|
351
|
+
|
|
352
|
+
Example:
|
|
353
|
+
>>> server = RESTAPIServer(host="0.0.0.0", port=8000)
|
|
354
|
+
>>> server.run() # Starts server
|
|
355
|
+
>>> # Visit http://0.0.0.0:8000/docs for API documentation
|
|
356
|
+
"""
|
|
357
|
+
|
|
358
|
+
def __init__(
|
|
359
|
+
self,
|
|
360
|
+
host: str = "127.0.0.1",
|
|
361
|
+
port: int = 8000,
|
|
362
|
+
max_sessions: int = 100,
|
|
363
|
+
enable_cors: bool = True,
|
|
364
|
+
cors_origins: list[str] | None = None,
|
|
365
|
+
api_key: str | None = None,
|
|
366
|
+
rate_limit: int | None = None,
|
|
367
|
+
):
|
|
368
|
+
"""Initialize REST API server.
|
|
369
|
+
|
|
370
|
+
Args:
|
|
371
|
+
host: Server host address.
|
|
372
|
+
port: Server port number.
|
|
373
|
+
max_sessions: Maximum concurrent sessions.
|
|
374
|
+
enable_cors: Enable CORS middleware.
|
|
375
|
+
cors_origins: Allowed CORS origins (default: all).
|
|
376
|
+
api_key: Optional API key for authentication.
|
|
377
|
+
rate_limit: Optional rate limit (requests per minute).
|
|
378
|
+
|
|
379
|
+
Raises:
|
|
380
|
+
ImportError: If FastAPI is not available.
|
|
381
|
+
"""
|
|
382
|
+
if not HAS_FASTAPI:
|
|
383
|
+
raise ImportError("FastAPI required. Install with: pip install 'fastapi[all]' uvicorn")
|
|
384
|
+
|
|
385
|
+
self.host = host
|
|
386
|
+
self.port = port
|
|
387
|
+
self.api_key = api_key
|
|
388
|
+
self.rate_limit = rate_limit
|
|
389
|
+
|
|
390
|
+
# Initialize session manager
|
|
391
|
+
self.session_manager = SessionManager(max_sessions=max_sessions)
|
|
392
|
+
|
|
393
|
+
# Initialize security
|
|
394
|
+
self._security = HTTPBearer(auto_error=False) if HAS_FASTAPI else None
|
|
395
|
+
|
|
396
|
+
# Create FastAPI app with dynamic version from package metadata (SSOT: pyproject.toml)
|
|
397
|
+
try:
|
|
398
|
+
from importlib.metadata import version
|
|
399
|
+
|
|
400
|
+
app_version = version("oscura")
|
|
401
|
+
except Exception:
|
|
402
|
+
app_version = "0.0.0+dev"
|
|
403
|
+
|
|
404
|
+
self.app = FastAPI(
|
|
405
|
+
title="Oscura REST API",
|
|
406
|
+
description="Hardware reverse engineering and protocol analysis API",
|
|
407
|
+
version=app_version,
|
|
408
|
+
docs_url="/docs",
|
|
409
|
+
redoc_url="/redoc",
|
|
410
|
+
openapi_url="/api/openapi.json",
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
# Add CORS middleware
|
|
414
|
+
if enable_cors:
|
|
415
|
+
origins = cors_origins or ["*"]
|
|
416
|
+
self.app.add_middleware(
|
|
417
|
+
CORSMiddleware,
|
|
418
|
+
allow_origins=origins,
|
|
419
|
+
allow_credentials=True,
|
|
420
|
+
allow_methods=["*"],
|
|
421
|
+
allow_headers=["*"],
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
# Register routes
|
|
425
|
+
self._register_routes()
|
|
426
|
+
|
|
427
|
+
def _create_auth_dependency(self) -> Any:
|
|
428
|
+
"""Create authentication dependency for route protection.
|
|
429
|
+
|
|
430
|
+
Returns:
|
|
431
|
+
FastAPI dependency that validates API key.
|
|
432
|
+
|
|
433
|
+
Security:
|
|
434
|
+
Implements Bearer token authentication (SEC-002 fix).
|
|
435
|
+
If api_key is None, all requests are allowed (development mode).
|
|
436
|
+
If api_key is set, requests MUST include valid Bearer token.
|
|
437
|
+
"""
|
|
438
|
+
|
|
439
|
+
async def verify_api_key(
|
|
440
|
+
credentials: HTTPAuthorizationCredentials | None = Security(self._security), # noqa: B008
|
|
441
|
+
) -> None:
|
|
442
|
+
"""Verify API key if authentication is configured."""
|
|
443
|
+
if not self.api_key:
|
|
444
|
+
return # No auth required if not configured
|
|
445
|
+
|
|
446
|
+
if not credentials or credentials.credentials != self.api_key:
|
|
447
|
+
raise HTTPException(
|
|
448
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
449
|
+
detail="Invalid or missing API key",
|
|
450
|
+
headers={"WWW-Authenticate": "Bearer"},
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
return Depends(verify_api_key)
|
|
454
|
+
|
|
455
|
+
def _register_routes(self) -> None:
|
|
456
|
+
"""Register API endpoints."""
|
|
457
|
+
self._register_health_route()
|
|
458
|
+
self._register_analyze_route()
|
|
459
|
+
self._register_sessions_routes()
|
|
460
|
+
self._register_protocols_route()
|
|
461
|
+
self._register_export_route()
|
|
462
|
+
|
|
463
|
+
def _register_health_route(self) -> None:
|
|
464
|
+
"""Register health check endpoint."""
|
|
465
|
+
|
|
466
|
+
@self.app.get("/api/health", tags=["Health"])
|
|
467
|
+
async def health_check() -> dict[str, Any]:
|
|
468
|
+
"""Health check endpoint with dynamic version from package metadata."""
|
|
469
|
+
try:
|
|
470
|
+
from importlib.metadata import version
|
|
471
|
+
|
|
472
|
+
current_version = version("oscura")
|
|
473
|
+
except Exception:
|
|
474
|
+
current_version = "0.0.0+dev"
|
|
475
|
+
|
|
476
|
+
return {
|
|
477
|
+
"status": "healthy",
|
|
478
|
+
"version": current_version,
|
|
479
|
+
"sessions_active": len(self.session_manager.sessions),
|
|
480
|
+
"timestamp": datetime.utcnow().isoformat(),
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
def _register_analyze_route(self) -> None:
|
|
484
|
+
"""Register analysis endpoint."""
|
|
485
|
+
|
|
486
|
+
@self.app.post(
|
|
487
|
+
"/api/v1/analyze",
|
|
488
|
+
tags=["Analysis"],
|
|
489
|
+
status_code=status.HTTP_202_ACCEPTED,
|
|
490
|
+
dependencies=[self._create_auth_dependency()],
|
|
491
|
+
)
|
|
492
|
+
async def analyze(
|
|
493
|
+
file: UploadFile,
|
|
494
|
+
background_tasks: BackgroundTasks,
|
|
495
|
+
protocol_hint: str | None = None,
|
|
496
|
+
auto_crc: bool = True,
|
|
497
|
+
detect_crypto: bool = True,
|
|
498
|
+
generate_tests: bool = True,
|
|
499
|
+
) -> dict[str, Any]:
|
|
500
|
+
"""Analyze uploaded file for protocol discovery."""
|
|
501
|
+
if not file.filename:
|
|
502
|
+
raise HTTPException(
|
|
503
|
+
status_code=status.HTTP_400_BAD_REQUEST, detail="Filename required"
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
file_data = await file.read()
|
|
507
|
+
options = {
|
|
508
|
+
"protocol_hint": protocol_hint,
|
|
509
|
+
"auto_crc": auto_crc,
|
|
510
|
+
"detect_crypto": detect_crypto,
|
|
511
|
+
"generate_tests": generate_tests,
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
try:
|
|
515
|
+
session_id = self.session_manager.create_session(file.filename, file_data, options)
|
|
516
|
+
except RuntimeError as e:
|
|
517
|
+
raise HTTPException(
|
|
518
|
+
status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail=str(e)
|
|
519
|
+
) from e
|
|
520
|
+
|
|
521
|
+
# FastAPI automatically injects BackgroundTasks
|
|
522
|
+
background_tasks.add_task(self._run_analysis, session_id)
|
|
523
|
+
|
|
524
|
+
return {
|
|
525
|
+
"session_id": session_id,
|
|
526
|
+
"status": "processing",
|
|
527
|
+
"message": "Analysis started",
|
|
528
|
+
"created_at": datetime.utcnow().isoformat(),
|
|
529
|
+
"estimated_duration": 30.0,
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
def _register_sessions_routes(self) -> None:
|
|
533
|
+
"""Register session management endpoints."""
|
|
534
|
+
|
|
535
|
+
@self.app.get(
|
|
536
|
+
"/api/v1/sessions",
|
|
537
|
+
tags=["Sessions"],
|
|
538
|
+
dependencies=[self._create_auth_dependency()],
|
|
539
|
+
)
|
|
540
|
+
async def list_sessions() -> dict[str, Any]:
|
|
541
|
+
"""List all active sessions."""
|
|
542
|
+
sessions = self.session_manager.list_sessions()
|
|
543
|
+
return {
|
|
544
|
+
"sessions": sessions,
|
|
545
|
+
"count": len(sessions),
|
|
546
|
+
"timestamp": datetime.utcnow().isoformat(),
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
@self.app.get(
|
|
550
|
+
"/api/v1/sessions/{session_id}",
|
|
551
|
+
tags=["Sessions"],
|
|
552
|
+
dependencies=[self._create_auth_dependency()],
|
|
553
|
+
)
|
|
554
|
+
async def get_session(session_id: str) -> dict[str, Any]:
|
|
555
|
+
"""Get session details."""
|
|
556
|
+
session = self.session_manager.get_session(session_id)
|
|
557
|
+
if not session:
|
|
558
|
+
raise HTTPException(
|
|
559
|
+
status_code=status.HTTP_404_NOT_FOUND, detail=f"Session {session_id} not found"
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
return self._build_session_response(session)
|
|
563
|
+
|
|
564
|
+
@self.app.delete(
|
|
565
|
+
"/api/v1/sessions/{session_id}",
|
|
566
|
+
tags=["Sessions"],
|
|
567
|
+
dependencies=[self._create_auth_dependency()],
|
|
568
|
+
)
|
|
569
|
+
async def delete_session(session_id: str) -> dict[str, Any]:
|
|
570
|
+
"""Delete a session."""
|
|
571
|
+
deleted = self.session_manager.delete_session(session_id)
|
|
572
|
+
if not deleted:
|
|
573
|
+
raise HTTPException(
|
|
574
|
+
status_code=status.HTTP_404_NOT_FOUND, detail=f"Session {session_id} not found"
|
|
575
|
+
)
|
|
576
|
+
|
|
577
|
+
return {
|
|
578
|
+
"message": "Session deleted",
|
|
579
|
+
"session_id": session_id,
|
|
580
|
+
"timestamp": datetime.utcnow().isoformat(),
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
def _register_protocols_route(self) -> None:
|
|
584
|
+
"""Register protocols listing endpoint."""
|
|
585
|
+
|
|
586
|
+
@self.app.get(
|
|
587
|
+
"/api/v1/protocols",
|
|
588
|
+
tags=["Protocols"],
|
|
589
|
+
dependencies=[self._create_auth_dependency()],
|
|
590
|
+
)
|
|
591
|
+
async def list_protocols() -> dict[str, Any]:
|
|
592
|
+
"""List all discovered protocols across sessions."""
|
|
593
|
+
protocols = self._extract_protocols_from_sessions()
|
|
594
|
+
return {
|
|
595
|
+
"protocols": protocols,
|
|
596
|
+
"count": len(protocols),
|
|
597
|
+
"timestamp": datetime.utcnow().isoformat(),
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
def _register_export_route(self) -> None:
|
|
601
|
+
"""Register export endpoint."""
|
|
602
|
+
|
|
603
|
+
@self.app.post(
|
|
604
|
+
"/api/v1/export/{export_format}",
|
|
605
|
+
tags=["Export"],
|
|
606
|
+
dependencies=[self._create_auth_dependency()],
|
|
607
|
+
)
|
|
608
|
+
async def export_results(session_id: str, export_format: str) -> dict[str, Any]:
|
|
609
|
+
"""Export analysis results in specified format."""
|
|
610
|
+
session = self._validate_session_for_export(session_id, export_format)
|
|
611
|
+
artifacts = self._serialize_artifacts(session["result"])
|
|
612
|
+
artifact_path = self._get_export_artifact_path(export_format, artifacts)
|
|
613
|
+
|
|
614
|
+
return {
|
|
615
|
+
"format": export_format,
|
|
616
|
+
"file_path": artifact_path,
|
|
617
|
+
"session_id": session_id,
|
|
618
|
+
"timestamp": datetime.utcnow().isoformat(),
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
def _build_session_response(self, session: dict[str, Any]) -> dict[str, Any]:
|
|
622
|
+
"""Build session response dict.
|
|
623
|
+
|
|
624
|
+
Args:
|
|
625
|
+
session: Session data from manager.
|
|
626
|
+
|
|
627
|
+
Returns:
|
|
628
|
+
Response dict with session details.
|
|
629
|
+
"""
|
|
630
|
+
response = {
|
|
631
|
+
"session_id": session["id"],
|
|
632
|
+
"status": session["status"],
|
|
633
|
+
"filename": session["filename"],
|
|
634
|
+
"file_hash": session["file_hash"],
|
|
635
|
+
"created_at": session["created_at"],
|
|
636
|
+
"updated_at": session["updated_at"],
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
if session["result"]:
|
|
640
|
+
result = session["result"]
|
|
641
|
+
response["protocol_spec"] = self._serialize_protocol_spec(result)
|
|
642
|
+
response["confidence_score"] = getattr(result, "confidence_score", 0.0)
|
|
643
|
+
response["artifacts"] = self._serialize_artifacts(result)
|
|
644
|
+
|
|
645
|
+
if session["error"]:
|
|
646
|
+
response["error"] = session["error"]
|
|
647
|
+
|
|
648
|
+
return response
|
|
649
|
+
|
|
650
|
+
def _extract_protocols_from_sessions(self) -> list[dict[str, Any]]:
|
|
651
|
+
"""Extract protocol information from all sessions.
|
|
652
|
+
|
|
653
|
+
Returns:
|
|
654
|
+
List of protocol dicts.
|
|
655
|
+
"""
|
|
656
|
+
protocols = []
|
|
657
|
+
for session in self.session_manager.sessions.values():
|
|
658
|
+
if session["result"]:
|
|
659
|
+
result = session["result"]
|
|
660
|
+
spec = getattr(result, "protocol_spec", None)
|
|
661
|
+
if spec:
|
|
662
|
+
protocols.append(
|
|
663
|
+
{
|
|
664
|
+
"session_id": session["id"],
|
|
665
|
+
"protocol_name": getattr(spec, "protocol_name", "unknown"),
|
|
666
|
+
"confidence": getattr(result, "confidence_score", 0.0),
|
|
667
|
+
"message_count": len(getattr(spec, "messages", [])),
|
|
668
|
+
"field_count": len(getattr(spec, "fields", [])),
|
|
669
|
+
}
|
|
670
|
+
)
|
|
671
|
+
return protocols
|
|
672
|
+
|
|
673
|
+
def _validate_session_for_export(self, session_id: str, export_format: str) -> dict[str, Any]:
|
|
674
|
+
"""Validate session exists and is ready for export.
|
|
675
|
+
|
|
676
|
+
Args:
|
|
677
|
+
session_id: Session identifier.
|
|
678
|
+
export_format: Export format.
|
|
679
|
+
|
|
680
|
+
Returns:
|
|
681
|
+
Session data.
|
|
682
|
+
|
|
683
|
+
Raises:
|
|
684
|
+
HTTPException: If validation fails.
|
|
685
|
+
"""
|
|
686
|
+
session = self.session_manager.get_session(session_id)
|
|
687
|
+
if not session:
|
|
688
|
+
raise HTTPException(
|
|
689
|
+
status_code=status.HTTP_404_NOT_FOUND, detail=f"Session {session_id} not found"
|
|
690
|
+
)
|
|
691
|
+
|
|
692
|
+
if session["status"] != "complete":
|
|
693
|
+
raise HTTPException(
|
|
694
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
695
|
+
detail=f"Session {session_id} not complete (status: {session['status']})",
|
|
696
|
+
)
|
|
697
|
+
|
|
698
|
+
valid_formats = ["wireshark", "scapy", "kaitai"]
|
|
699
|
+
if export_format not in valid_formats:
|
|
700
|
+
raise HTTPException(
|
|
701
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
702
|
+
detail=f"Invalid format. Must be one of: {valid_formats}",
|
|
703
|
+
)
|
|
704
|
+
|
|
705
|
+
return session
|
|
706
|
+
|
|
707
|
+
def _get_export_artifact_path(self, export_format: str, artifacts: dict[str, str]) -> str:
|
|
708
|
+
"""Get artifact path for export format.
|
|
709
|
+
|
|
710
|
+
Args:
|
|
711
|
+
export_format: Export format name.
|
|
712
|
+
artifacts: Artifacts dict from serialization.
|
|
713
|
+
|
|
714
|
+
Returns:
|
|
715
|
+
Artifact file path.
|
|
716
|
+
|
|
717
|
+
Raises:
|
|
718
|
+
HTTPException: If artifact not available.
|
|
719
|
+
"""
|
|
720
|
+
format_map = {
|
|
721
|
+
"wireshark": "dissector_path",
|
|
722
|
+
"scapy": "scapy_layer_path",
|
|
723
|
+
"kaitai": "kaitai_path",
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
artifact_key = format_map.get(export_format)
|
|
727
|
+
if not artifact_key or artifact_key not in artifacts:
|
|
728
|
+
raise HTTPException(
|
|
729
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
730
|
+
detail=f"No {export_format} artifact available",
|
|
731
|
+
)
|
|
732
|
+
|
|
733
|
+
return artifacts[artifact_key]
|
|
734
|
+
|
|
735
|
+
def _run_analysis(self, session_id: str) -> None:
|
|
736
|
+
"""Run protocol analysis in background.
|
|
737
|
+
|
|
738
|
+
Args:
|
|
739
|
+
session_id: Session identifier.
|
|
740
|
+
"""
|
|
741
|
+
session = self.session_manager.get_session(session_id)
|
|
742
|
+
if not session:
|
|
743
|
+
logger.error(f"Session {session_id} not found for analysis")
|
|
744
|
+
return
|
|
745
|
+
|
|
746
|
+
self.session_manager.update_session(session_id, "processing")
|
|
747
|
+
|
|
748
|
+
try:
|
|
749
|
+
# Import here to avoid circular imports
|
|
750
|
+
from oscura.workflows.complete_re import full_protocol_re
|
|
751
|
+
|
|
752
|
+
# Save uploaded file to temp location
|
|
753
|
+
with tempfile.NamedTemporaryFile(
|
|
754
|
+
delete=False, suffix=Path(session["filename"]).suffix
|
|
755
|
+
) as tmp_file:
|
|
756
|
+
tmp_file.write(session["file_data"])
|
|
757
|
+
tmp_path = tmp_file.name
|
|
758
|
+
|
|
759
|
+
# Run analysis
|
|
760
|
+
result = full_protocol_re(
|
|
761
|
+
captures=tmp_path,
|
|
762
|
+
protocol_hint=session["options"].get("protocol_hint"),
|
|
763
|
+
auto_crc=session["options"].get("auto_crc", True),
|
|
764
|
+
detect_crypto=session["options"].get("detect_crypto", True),
|
|
765
|
+
generate_tests=session["options"].get("generate_tests", True),
|
|
766
|
+
)
|
|
767
|
+
|
|
768
|
+
# Clean up temp file
|
|
769
|
+
Path(tmp_path).unlink(missing_ok=True)
|
|
770
|
+
|
|
771
|
+
# Update session with results
|
|
772
|
+
self.session_manager.update_session(session_id, "complete", result=result)
|
|
773
|
+
logger.info(f"Analysis complete for session {session_id}")
|
|
774
|
+
|
|
775
|
+
except Exception as e:
|
|
776
|
+
logger.exception(f"Analysis failed for session {session_id}: {e}")
|
|
777
|
+
self.session_manager.update_session(session_id, "error", error=str(e))
|
|
778
|
+
|
|
779
|
+
def _serialize_protocol_spec(self, result: CompleteREResult) -> dict[str, Any]:
|
|
780
|
+
"""Serialize protocol specification to dict.
|
|
781
|
+
|
|
782
|
+
Args:
|
|
783
|
+
result: Complete RE result.
|
|
784
|
+
|
|
785
|
+
Returns:
|
|
786
|
+
Serialized protocol spec.
|
|
787
|
+
"""
|
|
788
|
+
spec = result.protocol_spec
|
|
789
|
+
messages = getattr(spec, "messages", [])
|
|
790
|
+
fields = getattr(spec, "fields", [])
|
|
791
|
+
|
|
792
|
+
# Handle Mock objects and other non-list types in tests
|
|
793
|
+
try:
|
|
794
|
+
message_count = len(messages) if messages else 0
|
|
795
|
+
except TypeError:
|
|
796
|
+
message_count = 0
|
|
797
|
+
|
|
798
|
+
try:
|
|
799
|
+
field_count = len(fields) if fields else 0
|
|
800
|
+
except TypeError:
|
|
801
|
+
field_count = 0
|
|
802
|
+
fields = []
|
|
803
|
+
|
|
804
|
+
return {
|
|
805
|
+
"protocol_name": getattr(spec, "protocol_name", "unknown"),
|
|
806
|
+
"message_count": message_count,
|
|
807
|
+
"field_count": field_count,
|
|
808
|
+
"fields": [
|
|
809
|
+
{
|
|
810
|
+
"name": getattr(f, "name", ""),
|
|
811
|
+
"offset": getattr(f, "offset", 0),
|
|
812
|
+
"length": getattr(f, "length", 0),
|
|
813
|
+
"type": getattr(f, "field_type", ""),
|
|
814
|
+
"confidence": getattr(f, "confidence", 0.0),
|
|
815
|
+
}
|
|
816
|
+
for f in fields
|
|
817
|
+
],
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
def _serialize_artifacts(self, result: CompleteREResult) -> dict[str, str]:
|
|
821
|
+
"""Serialize artifact paths to dict.
|
|
822
|
+
|
|
823
|
+
Args:
|
|
824
|
+
result: Complete RE result.
|
|
825
|
+
|
|
826
|
+
Returns:
|
|
827
|
+
Dict of artifact type to path.
|
|
828
|
+
"""
|
|
829
|
+
artifacts = {}
|
|
830
|
+
if result.dissector_path:
|
|
831
|
+
artifacts["dissector_path"] = str(result.dissector_path)
|
|
832
|
+
if result.scapy_layer_path:
|
|
833
|
+
artifacts["scapy_layer_path"] = str(result.scapy_layer_path)
|
|
834
|
+
if result.kaitai_path:
|
|
835
|
+
artifacts["kaitai_path"] = str(result.kaitai_path)
|
|
836
|
+
if result.test_vectors_path:
|
|
837
|
+
artifacts["test_vectors_path"] = str(result.test_vectors_path)
|
|
838
|
+
if result.report_path:
|
|
839
|
+
artifacts["report_path"] = str(result.report_path)
|
|
840
|
+
return artifacts
|
|
841
|
+
|
|
842
|
+
def run(self, reload: bool = False) -> None:
|
|
843
|
+
"""Start the REST API server.
|
|
844
|
+
|
|
845
|
+
Args:
|
|
846
|
+
reload: Enable auto-reload for development.
|
|
847
|
+
"""
|
|
848
|
+
try:
|
|
849
|
+
import uvicorn
|
|
850
|
+
except ImportError as e:
|
|
851
|
+
raise ImportError("uvicorn required. Install with: pip install uvicorn") from e
|
|
852
|
+
|
|
853
|
+
logger.info(f"Starting Oscura REST API server on {self.host}:{self.port}")
|
|
854
|
+
logger.info(f"API documentation: http://{self.host}:{self.port}/docs")
|
|
855
|
+
|
|
856
|
+
uvicorn.run(
|
|
857
|
+
self.app,
|
|
858
|
+
host=self.host,
|
|
859
|
+
port=self.port,
|
|
860
|
+
reload=reload,
|
|
861
|
+
log_level="info",
|
|
862
|
+
)
|
|
863
|
+
|
|
864
|
+
|
|
865
|
+
# ============================================================================
|
|
866
|
+
# Command-Line Interface
|
|
867
|
+
# ============================================================================
|
|
868
|
+
|
|
869
|
+
|
|
870
|
+
def main() -> None:
|
|
871
|
+
"""Command-line interface for REST API server."""
|
|
872
|
+
import argparse
|
|
873
|
+
|
|
874
|
+
parser = argparse.ArgumentParser(
|
|
875
|
+
description="Oscura REST API Server",
|
|
876
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
877
|
+
)
|
|
878
|
+
parser.add_argument(
|
|
879
|
+
"--host",
|
|
880
|
+
type=str,
|
|
881
|
+
default="127.0.0.1",
|
|
882
|
+
help="Server host address (default: 127.0.0.1)",
|
|
883
|
+
)
|
|
884
|
+
parser.add_argument(
|
|
885
|
+
"--port",
|
|
886
|
+
type=int,
|
|
887
|
+
default=8000,
|
|
888
|
+
help="Server port number (default: 8000)",
|
|
889
|
+
)
|
|
890
|
+
parser.add_argument(
|
|
891
|
+
"--max-sessions",
|
|
892
|
+
type=int,
|
|
893
|
+
default=100,
|
|
894
|
+
help="Maximum concurrent sessions (default: 100)",
|
|
895
|
+
)
|
|
896
|
+
parser.add_argument(
|
|
897
|
+
"--reload",
|
|
898
|
+
action="store_true",
|
|
899
|
+
help="Enable auto-reload for development",
|
|
900
|
+
)
|
|
901
|
+
parser.add_argument(
|
|
902
|
+
"--no-cors",
|
|
903
|
+
action="store_true",
|
|
904
|
+
help="Disable CORS middleware",
|
|
905
|
+
)
|
|
906
|
+
|
|
907
|
+
args = parser.parse_args()
|
|
908
|
+
|
|
909
|
+
# Create and run server
|
|
910
|
+
server = RESTAPIServer(
|
|
911
|
+
host=args.host,
|
|
912
|
+
port=args.port,
|
|
913
|
+
max_sessions=args.max_sessions,
|
|
914
|
+
enable_cors=not args.no_cors,
|
|
915
|
+
)
|
|
916
|
+
|
|
917
|
+
server.run(reload=args.reload)
|
|
918
|
+
|
|
919
|
+
|
|
920
|
+
if __name__ == "__main__":
|
|
921
|
+
main()
|