oscura 0.8.0__py3-none-any.whl → 0.11.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.
Files changed (161) hide show
  1. oscura/__init__.py +19 -19
  2. oscura/__main__.py +4 -0
  3. oscura/analyzers/__init__.py +2 -0
  4. oscura/analyzers/digital/extraction.py +2 -3
  5. oscura/analyzers/digital/quality.py +1 -1
  6. oscura/analyzers/digital/timing.py +1 -1
  7. oscura/analyzers/ml/signal_classifier.py +6 -0
  8. oscura/analyzers/patterns/__init__.py +66 -0
  9. oscura/analyzers/power/basic.py +3 -3
  10. oscura/analyzers/power/soa.py +1 -1
  11. oscura/analyzers/power/switching.py +3 -3
  12. oscura/analyzers/signal_classification.py +529 -0
  13. oscura/analyzers/signal_integrity/sparams.py +3 -3
  14. oscura/analyzers/statistics/basic.py +10 -7
  15. oscura/analyzers/validation.py +1 -1
  16. oscura/analyzers/waveform/measurements.py +200 -156
  17. oscura/analyzers/waveform/measurements_with_uncertainty.py +91 -35
  18. oscura/analyzers/waveform/spectral.py +182 -84
  19. oscura/api/dsl/commands.py +15 -6
  20. oscura/api/server/templates/base.html +137 -146
  21. oscura/api/server/templates/export.html +84 -110
  22. oscura/api/server/templates/home.html +248 -267
  23. oscura/api/server/templates/protocols.html +44 -48
  24. oscura/api/server/templates/reports.html +27 -35
  25. oscura/api/server/templates/session_detail.html +68 -78
  26. oscura/api/server/templates/sessions.html +62 -72
  27. oscura/api/server/templates/waveforms.html +54 -64
  28. oscura/automotive/__init__.py +1 -1
  29. oscura/automotive/can/session.py +1 -1
  30. oscura/automotive/dbc/generator.py +638 -23
  31. oscura/automotive/dtc/data.json +17 -102
  32. oscura/automotive/flexray/fibex.py +9 -1
  33. oscura/automotive/uds/decoder.py +99 -6
  34. oscura/cli/analyze.py +8 -2
  35. oscura/cli/batch.py +36 -5
  36. oscura/cli/characterize.py +18 -4
  37. oscura/cli/export.py +47 -5
  38. oscura/cli/main.py +2 -0
  39. oscura/cli/onboarding/wizard.py +10 -6
  40. oscura/cli/pipeline.py +585 -0
  41. oscura/cli/visualize.py +6 -4
  42. oscura/convenience.py +400 -32
  43. oscura/core/measurement_result.py +286 -0
  44. oscura/core/progress.py +1 -1
  45. oscura/core/schemas/device_mapping.json +2 -8
  46. oscura/core/schemas/packet_format.json +4 -24
  47. oscura/core/schemas/protocol_definition.json +2 -12
  48. oscura/core/types.py +232 -239
  49. oscura/correlation/multi_protocol.py +1 -1
  50. oscura/export/legacy/__init__.py +11 -0
  51. oscura/export/legacy/wav.py +75 -0
  52. oscura/exporters/__init__.py +19 -0
  53. oscura/exporters/wireshark.py +809 -0
  54. oscura/hardware/acquisition/file.py +5 -19
  55. oscura/hardware/acquisition/saleae.py +10 -10
  56. oscura/hardware/acquisition/socketcan.py +4 -6
  57. oscura/hardware/acquisition/synthetic.py +1 -5
  58. oscura/hardware/acquisition/visa.py +6 -6
  59. oscura/hardware/security/side_channel_detector.py +5 -508
  60. oscura/inference/message_format.py +686 -1
  61. oscura/jupyter/display.py +2 -2
  62. oscura/jupyter/magic.py +3 -3
  63. oscura/loaders/__init__.py +17 -12
  64. oscura/loaders/binary.py +1 -1
  65. oscura/loaders/chipwhisperer.py +1 -2
  66. oscura/loaders/configurable.py +1 -1
  67. oscura/loaders/csv_loader.py +2 -2
  68. oscura/loaders/hdf5_loader.py +1 -1
  69. oscura/loaders/lazy.py +6 -1
  70. oscura/loaders/mmap_loader.py +0 -1
  71. oscura/loaders/numpy_loader.py +8 -7
  72. oscura/loaders/preprocessing.py +3 -5
  73. oscura/loaders/rigol.py +21 -7
  74. oscura/loaders/sigrok.py +2 -5
  75. oscura/loaders/tdms.py +3 -2
  76. oscura/loaders/tektronix.py +38 -32
  77. oscura/loaders/tss.py +20 -27
  78. oscura/loaders/validation.py +17 -10
  79. oscura/loaders/vcd.py +13 -8
  80. oscura/loaders/wav.py +1 -6
  81. oscura/pipeline/__init__.py +76 -0
  82. oscura/pipeline/handlers/__init__.py +165 -0
  83. oscura/pipeline/handlers/analyzers.py +1045 -0
  84. oscura/pipeline/handlers/decoders.py +899 -0
  85. oscura/pipeline/handlers/exporters.py +1103 -0
  86. oscura/pipeline/handlers/filters.py +891 -0
  87. oscura/pipeline/handlers/loaders.py +640 -0
  88. oscura/pipeline/handlers/transforms.py +768 -0
  89. oscura/reporting/formatting/measurements.py +55 -14
  90. oscura/reporting/templates/enhanced/protocol_re.html +504 -503
  91. oscura/sessions/legacy.py +49 -1
  92. oscura/side_channel/__init__.py +38 -57
  93. oscura/utils/builders/signal_builder.py +5 -5
  94. oscura/utils/comparison/compare.py +7 -9
  95. oscura/utils/comparison/golden.py +1 -1
  96. oscura/utils/filtering/convenience.py +2 -2
  97. oscura/utils/math/arithmetic.py +38 -62
  98. oscura/utils/math/interpolation.py +20 -20
  99. oscura/utils/pipeline/__init__.py +4 -17
  100. oscura/utils/progressive.py +1 -4
  101. oscura/utils/triggering/edge.py +1 -1
  102. oscura/utils/triggering/pattern.py +2 -2
  103. oscura/utils/triggering/pulse.py +2 -2
  104. oscura/utils/triggering/window.py +3 -3
  105. oscura/validation/hil_testing.py +11 -11
  106. oscura/visualization/__init__.py +46 -284
  107. oscura/visualization/batch.py +72 -433
  108. oscura/visualization/plot.py +542 -53
  109. oscura/visualization/styles.py +184 -318
  110. oscura/workflows/batch/advanced.py +1 -1
  111. oscura/workflows/batch/aggregate.py +12 -9
  112. oscura/workflows/complete_re.py +251 -23
  113. oscura/workflows/digital.py +27 -4
  114. oscura/workflows/multi_trace.py +136 -17
  115. oscura/workflows/waveform.py +11 -6
  116. oscura-0.11.0.dist-info/METADATA +460 -0
  117. {oscura-0.8.0.dist-info → oscura-0.11.0.dist-info}/RECORD +120 -145
  118. oscura/side_channel/dpa.py +0 -1025
  119. oscura/utils/optimization/__init__.py +0 -19
  120. oscura/utils/optimization/parallel.py +0 -443
  121. oscura/utils/optimization/search.py +0 -532
  122. oscura/utils/pipeline/base.py +0 -338
  123. oscura/utils/pipeline/composition.py +0 -248
  124. oscura/utils/pipeline/parallel.py +0 -449
  125. oscura/utils/pipeline/pipeline.py +0 -375
  126. oscura/utils/search/__init__.py +0 -16
  127. oscura/utils/search/anomaly.py +0 -424
  128. oscura/utils/search/context.py +0 -294
  129. oscura/utils/search/pattern.py +0 -288
  130. oscura/utils/storage/__init__.py +0 -61
  131. oscura/utils/storage/database.py +0 -1166
  132. oscura/visualization/accessibility.py +0 -526
  133. oscura/visualization/annotations.py +0 -371
  134. oscura/visualization/axis_scaling.py +0 -305
  135. oscura/visualization/colors.py +0 -451
  136. oscura/visualization/digital.py +0 -436
  137. oscura/visualization/eye.py +0 -571
  138. oscura/visualization/histogram.py +0 -281
  139. oscura/visualization/interactive.py +0 -1035
  140. oscura/visualization/jitter.py +0 -1042
  141. oscura/visualization/keyboard.py +0 -394
  142. oscura/visualization/layout.py +0 -400
  143. oscura/visualization/optimization.py +0 -1079
  144. oscura/visualization/palettes.py +0 -446
  145. oscura/visualization/power.py +0 -508
  146. oscura/visualization/power_extended.py +0 -955
  147. oscura/visualization/presets.py +0 -469
  148. oscura/visualization/protocols.py +0 -1246
  149. oscura/visualization/render.py +0 -223
  150. oscura/visualization/rendering.py +0 -444
  151. oscura/visualization/reverse_engineering.py +0 -838
  152. oscura/visualization/signal_integrity.py +0 -989
  153. oscura/visualization/specialized.py +0 -643
  154. oscura/visualization/spectral.py +0 -1226
  155. oscura/visualization/thumbnails.py +0 -340
  156. oscura/visualization/time_axis.py +0 -351
  157. oscura/visualization/waveform.py +0 -454
  158. oscura-0.8.0.dist-info/METADATA +0 -661
  159. {oscura-0.8.0.dist-info → oscura-0.11.0.dist-info}/WHEEL +0 -0
  160. {oscura-0.8.0.dist-info → oscura-0.11.0.dist-info}/entry_points.txt +0 -0
  161. {oscura-0.8.0.dist-info → oscura-0.11.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,288 +0,0 @@
1
- """Pattern search in digital traces.
2
-
3
-
4
- This module provides efficient bit pattern matching in digital signals
5
- with wildcard support via mask parameter.
6
- """
7
-
8
- from typing import cast
9
-
10
- import numpy as np
11
- from numpy.typing import NDArray
12
-
13
-
14
- def find_pattern(
15
- trace: NDArray[np.float64] | NDArray[np.uint8],
16
- pattern: int | NDArray[np.uint8],
17
- mask: int | NDArray[np.uint8] | None = None,
18
- *,
19
- threshold: float | None = None,
20
- min_spacing: int = 1,
21
- ) -> list[tuple[int, NDArray[np.uint8]]]:
22
- """Find occurrences of bit patterns in digital traces.
23
-
24
- : Pattern search with wildcard support via mask.
25
- Works on both raw analog traces (with threshold) and decoded digital data.
26
-
27
- Args:
28
- trace: Input trace array. If analog (float), threshold is required.
29
- If already digital (uint8), threshold is ignored.
30
- pattern: Bit pattern to search for. Can be:
31
- - Integer: e.g., 0b10101010 (8-bit pattern)
32
- - Array: sequence of bytes to match
33
- mask: Optional mask for wildcard matching. Bits set to 0 in mask
34
- are "don't care" positions. Can be:
35
- - Integer: e.g., 0xFF (all bits matter)
36
- - Array: per-byte masks
37
- If None, all bits must match (equivalent to all 1s).
38
- threshold: Threshold for converting analog to digital (required if
39
- trace is analog). Typically mid-level of logic family.
40
- min_spacing: Minimum samples between detected patterns to avoid
41
- overlapping matches (default: 1)
42
-
43
- Returns:
44
- List of (index, match) tuples where:
45
- - index: Starting sample index of the pattern
46
- - match: The actual matched bit sequence as uint8 array
47
-
48
- Raises:
49
- ValueError: If analog trace provided without threshold
50
- ValueError: If pattern is empty
51
-
52
- Examples:
53
- >>> # Find 0xAA pattern in analog trace
54
- >>> import numpy as np
55
- >>> trace = np.array([0, 1, 0, 1, 0, 1, 0, 1, 0, 0])
56
- >>> matches = find_pattern(trace, 0b10101010, threshold=0.5)
57
- >>> print(f"Found {len(matches)} matches")
58
-
59
- >>> # Wildcard search: find 0b1010xxxx (x = don't care)
60
- >>> pattern = 0b10100000
61
- >>> mask = 0b11110000 # Only upper 4 bits matter
62
- >>> matches = find_pattern(trace, pattern, mask, threshold=0.5)
63
-
64
- >>> # Search in already-decoded digital data
65
- >>> digital = np.array([0xAA, 0x55, 0xAA, 0x00], dtype=np.uint8)
66
- >>> matches = find_pattern(digital, 0xAA)
67
-
68
- Notes:
69
- - For analog traces, values >= threshold are interpreted as '1'
70
- - Mask bits: 1 = must match, 0 = don't care
71
- - Overlapping patterns can be filtered with min_spacing > 1
72
- - Returns empty list if no matches found
73
-
74
- References:
75
- SRCH-001: Pattern Search
76
- """
77
- if trace.size == 0:
78
- return []
79
-
80
- # Phase 1: Input normalization
81
- pattern_arr = _normalize_pattern(pattern)
82
- mask_arr = _normalize_mask(mask, pattern_arr)
83
-
84
- # Phase 2: Convert trace to digital format
85
- digital_packed = _convert_trace_to_digital(trace, threshold)
86
-
87
- if digital_packed.size < pattern_arr.size:
88
- return []
89
-
90
- # Phase 3: Sliding window search with mask
91
- return _sliding_window_search(digital_packed, pattern_arr, mask_arr, min_spacing)
92
-
93
-
94
- def _normalize_pattern(pattern: int | NDArray[np.uint8]) -> NDArray[np.uint8]:
95
- """Normalize pattern input to numpy array.
96
-
97
- Args:
98
- pattern: Pattern as integer or array.
99
-
100
- Returns:
101
- Pattern as uint8 numpy array.
102
-
103
- Raises:
104
- ValueError: If pattern is negative or empty.
105
-
106
- Example:
107
- >>> _normalize_pattern(0xAA)
108
- array([170], dtype=uint8)
109
- >>> _normalize_pattern(0x1234)
110
- array([18, 52], dtype=uint8)
111
- """
112
- if isinstance(pattern, int):
113
- if pattern < 0:
114
- raise ValueError("Pattern must be non-negative")
115
- # Convert to byte array (variable length based on value)
116
- pattern_bytes = []
117
- if pattern == 0:
118
- pattern_bytes = [0]
119
- else:
120
- temp = pattern
121
- while temp > 0:
122
- pattern_bytes.insert(0, temp & 0xFF)
123
- temp >>= 8
124
- pattern_arr = np.array(pattern_bytes, dtype=np.uint8)
125
- else:
126
- pattern_arr = np.asarray(pattern, dtype=np.uint8)
127
-
128
- if pattern_arr.size == 0:
129
- raise ValueError("Pattern cannot be empty")
130
-
131
- return pattern_arr
132
-
133
-
134
- def _normalize_mask(
135
- mask: int | NDArray[np.uint8] | None, pattern_arr: NDArray[np.uint8]
136
- ) -> NDArray[np.uint8]:
137
- """Normalize mask input to numpy array matching pattern length.
138
-
139
- Args:
140
- mask: Mask as integer, array, or None.
141
- pattern_arr: Pattern array to match length to.
142
-
143
- Returns:
144
- Mask as uint8 numpy array with same length as pattern.
145
-
146
- Raises:
147
- ValueError: If mask and pattern have different lengths.
148
-
149
- Example:
150
- >>> pattern = np.array([0xAA, 0x55], dtype=np.uint8)
151
- >>> _normalize_mask(0xFF, pattern)
152
- array([255, 255], dtype=uint8)
153
- >>> _normalize_mask(None, pattern)
154
- array([255, 255], dtype=uint8)
155
- """
156
- if mask is not None:
157
- if isinstance(mask, int):
158
- mask_bytes: list[int] = []
159
- temp = mask
160
- # Match pattern length
161
- for _ in range(len(pattern_arr)):
162
- mask_bytes.insert(0, temp & 0xFF)
163
- temp >>= 8
164
- mask_arr = np.array(mask_bytes, dtype=np.uint8)
165
- else:
166
- mask_arr = np.asarray(mask, dtype=np.uint8)
167
-
168
- # Ensure mask and pattern have same length
169
- if mask_arr.size != pattern_arr.size:
170
- raise ValueError("Mask and pattern must have same length")
171
- else:
172
- # Default: all bits matter
173
- mask_arr = np.full(pattern_arr.size, 0xFF, dtype=np.uint8)
174
-
175
- return mask_arr
176
-
177
-
178
- def _convert_trace_to_digital(
179
- trace: NDArray[np.float64] | NDArray[np.uint8], threshold: float | None
180
- ) -> NDArray[np.uint8]:
181
- """Convert trace to digital packed format.
182
-
183
- If trace is already digital (uint8), returns as-is.
184
- If trace is analog (float), converts using threshold and packs bits.
185
-
186
- Args:
187
- trace: Input trace (analog or digital).
188
- threshold: Threshold for analog-to-digital conversion.
189
-
190
- Returns:
191
- Digital trace as packed uint8 array.
192
-
193
- Raises:
194
- ValueError: If analog trace provided without threshold.
195
-
196
- Example:
197
- >>> analog = np.array([1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0])
198
- >>> _convert_trace_to_digital(analog, 0.5)
199
- array([170], dtype=uint8) # 0b10101010 = 0xAA
200
- """
201
- if trace.dtype != np.uint8:
202
- if threshold is None:
203
- raise ValueError(
204
- "Threshold required for analog trace conversion. "
205
- "Provide threshold parameter or pre-convert to digital."
206
- )
207
- # Simple threshold conversion: >= threshold is 1
208
- digital = (trace >= threshold).astype(np.uint8)
209
- # Pack bits into bytes (8 samples per byte)
210
- # Pad to multiple of 8
211
- n_pad = (8 - len(digital) % 8) % 8
212
- if n_pad:
213
- digital = np.pad(digital, (0, n_pad), constant_values=0)
214
- # Pack bits
215
- digital_packed: NDArray[np.uint8] = np.packbits(digital, bitorder="big")
216
- else:
217
- digital_packed = cast("NDArray[np.uint8]", trace)
218
-
219
- return digital_packed
220
-
221
-
222
- def _sliding_window_search(
223
- digital_packed: NDArray[np.uint8],
224
- pattern_arr: NDArray[np.uint8],
225
- mask_arr: NDArray[np.uint8],
226
- min_spacing: int,
227
- ) -> list[tuple[int, NDArray[np.uint8]]]:
228
- """Perform sliding window pattern matching with mask.
229
-
230
- Args:
231
- digital_packed: Digital trace data.
232
- pattern_arr: Pattern to search for.
233
- mask_arr: Mask for wildcard matching.
234
- min_spacing: Minimum spacing between matches.
235
-
236
- Returns:
237
- List of (index, matched_data) tuples.
238
-
239
- Example:
240
- >>> data = np.array([0xAA, 0x55, 0xAA], dtype=np.uint8)
241
- >>> pattern = np.array([0xAA], dtype=np.uint8)
242
- >>> mask = np.array([0xFF], dtype=np.uint8)
243
- >>> _sliding_window_search(data, pattern, mask, 1)
244
- [(0, array([170], dtype=uint8)), (2, array([170], dtype=uint8))]
245
- """
246
- matches: list[tuple[int, NDArray[np.uint8]]] = []
247
- i = 0
248
-
249
- while i <= len(digital_packed) - len(pattern_arr):
250
- window = digital_packed[i : i + len(pattern_arr)]
251
-
252
- # Apply mask and compare
253
- if _matches_pattern(window, pattern_arr, mask_arr):
254
- # NECESSARY COPY: window is a numpy view that gets reused.
255
- # Without .copy(), all matches would reference same memory location.
256
- # Could optimize: store indices + lazy evaluation instead of copies.
257
- matches.append((i, window.copy()))
258
- # Skip ahead by min_spacing to avoid overlapping matches
259
- i += max(1, min_spacing)
260
- else:
261
- i += 1
262
-
263
- return matches
264
-
265
-
266
- def _matches_pattern(
267
- window: NDArray[np.uint8], pattern: NDArray[np.uint8], mask: NDArray[np.uint8]
268
- ) -> bool:
269
- """Check if window matches pattern with mask.
270
-
271
- Args:
272
- window: Data window to check.
273
- pattern: Pattern to match.
274
- mask: Mask for wildcard positions.
275
-
276
- Returns:
277
- True if window matches pattern under mask.
278
-
279
- Example:
280
- >>> window = np.array([0xA5], dtype=np.uint8)
281
- >>> pattern = np.array([0xA0], dtype=np.uint8)
282
- >>> mask = np.array([0xF0], dtype=np.uint8)
283
- >>> _matches_pattern(window, pattern, mask)
284
- True
285
- """
286
- masked_window = window & mask
287
- masked_pattern = pattern & mask
288
- return bool(np.array_equal(masked_window, masked_pattern))
@@ -1,61 +0,0 @@
1
- """Storage backends for persisting analysis results.
2
-
3
- This module provides database backends for storing and querying
4
- reverse engineering session data, protocol analysis results,
5
- and decoded messages.
6
-
7
- Example:
8
- >>> from oscura.utils.storage import DatabaseBackend, DatabaseConfig
9
- >>>
10
- >>> # Create database backend
11
- >>> config = DatabaseConfig(url="sqlite:///analysis.db")
12
- >>> db = DatabaseBackend(config)
13
- >>>
14
- >>> # Create project and session
15
- >>> project_id = db.create_project("IoT Device RE", "Unknown protocol analysis")
16
- >>> session_id = db.create_session(project_id, "blackbox", {"capture": "device.bin"})
17
- >>>
18
- >>> # Store protocol analysis
19
- >>> protocol_id = db.store_protocol(
20
- ... session_id,
21
- ... name="IoT Protocol",
22
- ... spec_json={"fields": [...]},
23
- ... confidence=0.85
24
- ... )
25
- >>>
26
- >>> # Store decoded messages
27
- >>> db.store_message(protocol_id, timestamp=0.0, data=b"\\xaa\\x55", decoded={"id": 1})
28
- >>>
29
- >>> # Query results
30
- >>> protocols = db.find_protocols(min_confidence=0.8)
31
- >>> sessions = db.get_sessions(project_id)
32
- >>> messages = db.query_messages(protocol_id, time_range=(0.0, 1.0))
33
-
34
- Available classes:
35
- - DatabaseConfig: Configuration dataclass
36
- - DatabaseBackend: Main database interface
37
- - Project/Session/Protocol/Message: Result dataclasses
38
- - QueryResult: Paginated query results
39
- """
40
-
41
- from oscura.utils.storage.database import (
42
- AnalysisResult,
43
- DatabaseBackend,
44
- DatabaseConfig,
45
- Message,
46
- Project,
47
- Protocol,
48
- QueryResult,
49
- Session,
50
- )
51
-
52
- __all__ = [
53
- "AnalysisResult",
54
- "DatabaseBackend",
55
- "DatabaseConfig",
56
- "Message",
57
- "Project",
58
- "Protocol",
59
- "QueryResult",
60
- "Session",
61
- ]