oscura 0.7.0__py3-none-any.whl → 0.10.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 (175) hide show
  1. oscura/__init__.py +19 -19
  2. oscura/analyzers/__init__.py +2 -0
  3. oscura/analyzers/digital/extraction.py +2 -3
  4. oscura/analyzers/digital/quality.py +1 -1
  5. oscura/analyzers/digital/timing.py +1 -1
  6. oscura/analyzers/eye/__init__.py +5 -1
  7. oscura/analyzers/eye/generation.py +501 -0
  8. oscura/analyzers/jitter/__init__.py +6 -6
  9. oscura/analyzers/jitter/timing.py +419 -0
  10. oscura/analyzers/patterns/__init__.py +94 -0
  11. oscura/analyzers/patterns/reverse_engineering.py +991 -0
  12. oscura/analyzers/power/__init__.py +35 -12
  13. oscura/analyzers/power/basic.py +3 -3
  14. oscura/analyzers/power/soa.py +1 -1
  15. oscura/analyzers/power/switching.py +3 -3
  16. oscura/analyzers/signal_classification.py +529 -0
  17. oscura/analyzers/signal_integrity/sparams.py +3 -3
  18. oscura/analyzers/statistics/__init__.py +4 -0
  19. oscura/analyzers/statistics/basic.py +152 -0
  20. oscura/analyzers/statistics/correlation.py +47 -6
  21. oscura/analyzers/validation.py +1 -1
  22. oscura/analyzers/waveform/__init__.py +2 -0
  23. oscura/analyzers/waveform/measurements.py +329 -163
  24. oscura/analyzers/waveform/measurements_with_uncertainty.py +91 -35
  25. oscura/analyzers/waveform/spectral.py +498 -54
  26. oscura/api/dsl/commands.py +15 -6
  27. oscura/api/server/templates/base.html +137 -146
  28. oscura/api/server/templates/export.html +84 -110
  29. oscura/api/server/templates/home.html +248 -267
  30. oscura/api/server/templates/protocols.html +44 -48
  31. oscura/api/server/templates/reports.html +27 -35
  32. oscura/api/server/templates/session_detail.html +68 -78
  33. oscura/api/server/templates/sessions.html +62 -72
  34. oscura/api/server/templates/waveforms.html +54 -64
  35. oscura/automotive/__init__.py +1 -1
  36. oscura/automotive/can/session.py +1 -1
  37. oscura/automotive/dbc/generator.py +638 -23
  38. oscura/automotive/dtc/data.json +102 -17
  39. oscura/automotive/uds/decoder.py +99 -6
  40. oscura/cli/analyze.py +8 -2
  41. oscura/cli/batch.py +36 -5
  42. oscura/cli/characterize.py +18 -4
  43. oscura/cli/export.py +47 -5
  44. oscura/cli/main.py +2 -0
  45. oscura/cli/onboarding/wizard.py +10 -6
  46. oscura/cli/pipeline.py +585 -0
  47. oscura/cli/visualize.py +6 -4
  48. oscura/convenience.py +400 -32
  49. oscura/core/config/loader.py +0 -1
  50. oscura/core/measurement_result.py +286 -0
  51. oscura/core/progress.py +1 -1
  52. oscura/core/schemas/device_mapping.json +8 -2
  53. oscura/core/schemas/packet_format.json +24 -4
  54. oscura/core/schemas/protocol_definition.json +12 -2
  55. oscura/core/types.py +300 -199
  56. oscura/correlation/multi_protocol.py +1 -1
  57. oscura/export/legacy/__init__.py +11 -0
  58. oscura/export/legacy/wav.py +75 -0
  59. oscura/exporters/__init__.py +19 -0
  60. oscura/exporters/wireshark.py +809 -0
  61. oscura/hardware/acquisition/file.py +5 -19
  62. oscura/hardware/acquisition/saleae.py +10 -10
  63. oscura/hardware/acquisition/socketcan.py +4 -6
  64. oscura/hardware/acquisition/synthetic.py +1 -5
  65. oscura/hardware/acquisition/visa.py +6 -6
  66. oscura/hardware/security/side_channel_detector.py +5 -508
  67. oscura/inference/message_format.py +686 -1
  68. oscura/jupyter/display.py +2 -2
  69. oscura/jupyter/magic.py +3 -3
  70. oscura/loaders/__init__.py +17 -12
  71. oscura/loaders/binary.py +1 -1
  72. oscura/loaders/chipwhisperer.py +1 -2
  73. oscura/loaders/configurable.py +1 -1
  74. oscura/loaders/csv_loader.py +2 -2
  75. oscura/loaders/hdf5_loader.py +1 -1
  76. oscura/loaders/lazy.py +6 -1
  77. oscura/loaders/mmap_loader.py +0 -1
  78. oscura/loaders/numpy_loader.py +8 -7
  79. oscura/loaders/preprocessing.py +3 -5
  80. oscura/loaders/rigol.py +21 -7
  81. oscura/loaders/sigrok.py +2 -5
  82. oscura/loaders/tdms.py +3 -2
  83. oscura/loaders/tektronix.py +38 -32
  84. oscura/loaders/tss.py +20 -27
  85. oscura/loaders/vcd.py +13 -8
  86. oscura/loaders/wav.py +1 -6
  87. oscura/pipeline/__init__.py +76 -0
  88. oscura/pipeline/handlers/__init__.py +165 -0
  89. oscura/pipeline/handlers/analyzers.py +1045 -0
  90. oscura/pipeline/handlers/decoders.py +899 -0
  91. oscura/pipeline/handlers/exporters.py +1103 -0
  92. oscura/pipeline/handlers/filters.py +891 -0
  93. oscura/pipeline/handlers/loaders.py +640 -0
  94. oscura/pipeline/handlers/transforms.py +768 -0
  95. oscura/reporting/__init__.py +88 -1
  96. oscura/reporting/automation.py +348 -0
  97. oscura/reporting/citations.py +374 -0
  98. oscura/reporting/core.py +54 -0
  99. oscura/reporting/formatting/__init__.py +11 -0
  100. oscura/reporting/formatting/measurements.py +320 -0
  101. oscura/reporting/html.py +57 -0
  102. oscura/reporting/interpretation.py +431 -0
  103. oscura/reporting/summary.py +329 -0
  104. oscura/reporting/templates/enhanced/protocol_re.html +504 -503
  105. oscura/reporting/visualization.py +542 -0
  106. oscura/side_channel/__init__.py +38 -57
  107. oscura/utils/builders/signal_builder.py +5 -5
  108. oscura/utils/comparison/compare.py +7 -9
  109. oscura/utils/comparison/golden.py +1 -1
  110. oscura/utils/filtering/convenience.py +2 -2
  111. oscura/utils/math/arithmetic.py +38 -62
  112. oscura/utils/math/interpolation.py +20 -20
  113. oscura/utils/pipeline/__init__.py +4 -17
  114. oscura/utils/progressive.py +1 -4
  115. oscura/utils/triggering/edge.py +1 -1
  116. oscura/utils/triggering/pattern.py +2 -2
  117. oscura/utils/triggering/pulse.py +2 -2
  118. oscura/utils/triggering/window.py +3 -3
  119. oscura/validation/hil_testing.py +11 -11
  120. oscura/visualization/__init__.py +47 -284
  121. oscura/visualization/batch.py +160 -0
  122. oscura/visualization/plot.py +542 -53
  123. oscura/visualization/styles.py +184 -318
  124. oscura/workflows/__init__.py +2 -0
  125. oscura/workflows/batch/advanced.py +1 -1
  126. oscura/workflows/batch/aggregate.py +7 -8
  127. oscura/workflows/complete_re.py +251 -23
  128. oscura/workflows/digital.py +27 -4
  129. oscura/workflows/multi_trace.py +136 -17
  130. oscura/workflows/waveform.py +788 -0
  131. {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/METADATA +59 -79
  132. {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/RECORD +135 -149
  133. oscura/side_channel/dpa.py +0 -1025
  134. oscura/utils/optimization/__init__.py +0 -19
  135. oscura/utils/optimization/parallel.py +0 -443
  136. oscura/utils/optimization/search.py +0 -532
  137. oscura/utils/pipeline/base.py +0 -338
  138. oscura/utils/pipeline/composition.py +0 -248
  139. oscura/utils/pipeline/parallel.py +0 -449
  140. oscura/utils/pipeline/pipeline.py +0 -375
  141. oscura/utils/search/__init__.py +0 -16
  142. oscura/utils/search/anomaly.py +0 -424
  143. oscura/utils/search/context.py +0 -294
  144. oscura/utils/search/pattern.py +0 -288
  145. oscura/utils/storage/__init__.py +0 -61
  146. oscura/utils/storage/database.py +0 -1166
  147. oscura/visualization/accessibility.py +0 -526
  148. oscura/visualization/annotations.py +0 -371
  149. oscura/visualization/axis_scaling.py +0 -305
  150. oscura/visualization/colors.py +0 -451
  151. oscura/visualization/digital.py +0 -436
  152. oscura/visualization/eye.py +0 -571
  153. oscura/visualization/histogram.py +0 -281
  154. oscura/visualization/interactive.py +0 -1035
  155. oscura/visualization/jitter.py +0 -1042
  156. oscura/visualization/keyboard.py +0 -394
  157. oscura/visualization/layout.py +0 -400
  158. oscura/visualization/optimization.py +0 -1079
  159. oscura/visualization/palettes.py +0 -446
  160. oscura/visualization/power.py +0 -508
  161. oscura/visualization/power_extended.py +0 -955
  162. oscura/visualization/presets.py +0 -469
  163. oscura/visualization/protocols.py +0 -1246
  164. oscura/visualization/render.py +0 -223
  165. oscura/visualization/rendering.py +0 -444
  166. oscura/visualization/reverse_engineering.py +0 -838
  167. oscura/visualization/signal_integrity.py +0 -989
  168. oscura/visualization/specialized.py +0 -643
  169. oscura/visualization/spectral.py +0 -1226
  170. oscura/visualization/thumbnails.py +0 -340
  171. oscura/visualization/time_axis.py +0 -351
  172. oscura/visualization/waveform.py +0 -454
  173. {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/WHEEL +0 -0
  174. {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/entry_points.txt +0 -0
  175. {oscura-0.7.0.dist-info → oscura-0.10.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,838 +0,0 @@
1
- """Reverse Engineering Pipeline Visualization Module.
2
-
3
- This module provides comprehensive visualization functions for reverse engineering
4
- pipeline results, including message type distributions, field layouts, confidence
5
- heatmaps, protocol detection scores, and pipeline performance metrics.
6
-
7
- Functions:
8
- plot_re_summary: Multi-panel dashboard of RE pipeline results
9
- plot_message_type_distribution: Pie/bar chart of discovered message types
10
- plot_message_field_layout: Visual field layout with byte positions
11
- plot_field_confidence_heatmap: Heatmap of field inference confidence scores
12
- plot_protocol_candidates: Bar chart of protocol detection scores
13
- plot_crc_parameters: Visualization of detected CRC parameters
14
- plot_pipeline_timing: Performance metrics for each pipeline stage
15
- """
16
-
17
- from __future__ import annotations
18
-
19
- from typing import TYPE_CHECKING, Any
20
-
21
- import matplotlib.pyplot as plt
22
- import numpy as np
23
- from matplotlib.patches import Rectangle
24
-
25
- if TYPE_CHECKING:
26
- from matplotlib.figure import Figure
27
-
28
- from oscura.inference.crc_reverse import CRCParameters
29
- from oscura.inference.message_format import InferredField, MessageSchema
30
- from oscura.utils.pipeline.reverse_engineering import (
31
- MessageTypeInfo,
32
- ProtocolCandidate,
33
- REAnalysisResult,
34
- )
35
-
36
- __all__ = [
37
- "plot_crc_parameters",
38
- "plot_field_confidence_heatmap",
39
- "plot_message_field_layout",
40
- "plot_message_type_distribution",
41
- "plot_pipeline_timing",
42
- "plot_protocol_candidates",
43
- "plot_re_summary",
44
- ]
45
-
46
-
47
- def plot_re_summary(
48
- result: REAnalysisResult,
49
- *,
50
- figsize: tuple[float, float] = (16, 10),
51
- title: str | None = None,
52
- ) -> Figure:
53
- """Create multi-panel dashboard showing RE pipeline results overview.
54
-
55
- Displays a comprehensive summary of the reverse engineering analysis including:
56
- - Message type distribution
57
- - Protocol candidates
58
- - Pipeline timing
59
- - Key statistics
60
-
61
- Args:
62
- result: REAnalysisResult from pipeline analysis.
63
- figsize: Figure size (width, height) in inches.
64
- title: Optional custom title for the dashboard.
65
-
66
- Returns:
67
- Matplotlib figure object.
68
-
69
- Example:
70
- >>> from oscura.utils.pipeline.reverse_engineering import REPipeline
71
- >>> pipeline = REPipeline()
72
- >>> result = pipeline.analyze(data)
73
- >>> fig = plot_re_summary(result)
74
- >>> fig.savefig("re_summary.png", dpi=300)
75
- """
76
- fig = plt.figure(figsize=figsize)
77
-
78
- # Create grid layout
79
- gs = fig.add_gridspec(2, 3, hspace=0.3, wspace=0.3)
80
-
81
- # Panel 1: Summary statistics (top-left)
82
- ax1 = fig.add_subplot(gs[0, 0])
83
- _plot_summary_stats(ax1, result)
84
-
85
- # Panel 2: Message type distribution (top-center)
86
- ax2 = fig.add_subplot(gs[0, 1])
87
- _plot_message_types_panel(ax2, result.message_types)
88
-
89
- # Panel 3: Protocol candidates (top-right)
90
- ax3 = fig.add_subplot(gs[0, 2])
91
- _plot_protocol_panel(ax3, result.protocol_candidates)
92
-
93
- # Panel 4: Pipeline timing (bottom-left and center)
94
- ax4 = fig.add_subplot(gs[1, :2])
95
- _plot_timing_panel(ax4, result.statistics)
96
-
97
- # Panel 5: Warnings/info (bottom-right)
98
- ax5 = fig.add_subplot(gs[1, 2])
99
- _plot_warnings_panel(ax5, result)
100
-
101
- # Set main title
102
- if title:
103
- fig.suptitle(title, fontsize=14, fontweight="bold")
104
- else:
105
- fig.suptitle("Reverse Engineering Analysis Summary", fontsize=14, fontweight="bold")
106
-
107
- return fig
108
-
109
-
110
- def _get_field_type_colors() -> dict[str, str]:
111
- """Get color map for field types."""
112
- return {
113
- "constant": "#4CAF50", # Green
114
- "counter": "#2196F3", # Blue
115
- "timestamp": "#9C27B0", # Purple
116
- "length": "#FF9800", # Orange
117
- "checksum": "#F44336", # Red
118
- "data": "#607D8B", # Gray
119
- "unknown": "#9E9E9E", # Light gray
120
- }
121
-
122
-
123
- def _draw_field_rectangles(
124
- ax: Any,
125
- fields: list[InferredField],
126
- total_bytes: int,
127
- bar_height: float,
128
- y_center: float,
129
- type_colors: dict[str, str],
130
- ) -> None:
131
- """Draw field rectangles with labels."""
132
- for field in fields:
133
- color = type_colors.get(field.field_type, "#9E9E9E")
134
- width = field.size / total_bytes
135
-
136
- rect = Rectangle(
137
- (field.offset / total_bytes, y_center - bar_height / 2),
138
- width,
139
- bar_height,
140
- facecolor=color,
141
- edgecolor="black",
142
- linewidth=1.5,
143
- )
144
- ax.add_patch(rect)
145
-
146
- x_center = (field.offset + field.size / 2) / total_bytes
147
- label = f"{field.name}\n({field.field_type})" if field.size > 2 else field.field_type[:3]
148
-
149
- ax.text(
150
- x_center,
151
- y_center,
152
- label,
153
- ha="center",
154
- va="center",
155
- fontsize=9 if field.size > 2 else 7,
156
- fontweight="bold",
157
- color="white",
158
- )
159
-
160
-
161
- def _add_field_offsets(
162
- ax: Any,
163
- fields: list[InferredField],
164
- total_bytes: int,
165
- bar_height: float,
166
- y_center: float,
167
- ) -> None:
168
- """Add byte offset labels."""
169
- for field in fields:
170
- ax.text(
171
- field.offset / total_bytes,
172
- y_center - bar_height / 2 - 0.08,
173
- f"{field.offset}",
174
- ha="center",
175
- va="top",
176
- fontsize=8,
177
- )
178
-
179
- ax.text(
180
- 1.0,
181
- y_center - bar_height / 2 - 0.08,
182
- f"{total_bytes}",
183
- ha="center",
184
- va="top",
185
- fontsize=8,
186
- )
187
-
188
-
189
- def _add_field_legend(ax: Any, fields: list[InferredField], type_colors: dict[str, str]) -> None:
190
- """Add legend for field types."""
191
- legend_elements = [
192
- Rectangle((0, 0), 1, 1, facecolor=color, label=ftype)
193
- for ftype, color in type_colors.items()
194
- if any(f.field_type == ftype for f in fields)
195
- ]
196
- ax.legend(handles=legend_elements, loc="upper right", fontsize=8)
197
-
198
-
199
- def _format_layout_axes(ax: Any, total_bytes: int, title: str | None) -> None:
200
- """Format axes for layout plot."""
201
- ax.set_xlim(-0.02, 1.02)
202
- ax.set_ylim(0, 1)
203
- ax.set_aspect("equal")
204
- ax.axis("off")
205
-
206
- if title:
207
- ax.set_title(title, fontsize=12, fontweight="bold", pad=20)
208
- else:
209
- ax.set_title(
210
- f"Message Field Layout ({total_bytes} bytes)", fontsize=12, fontweight="bold", pad=20
211
- )
212
-
213
-
214
- def plot_message_type_distribution(
215
- message_types: list[MessageTypeInfo],
216
- *,
217
- figsize: tuple[float, float] = (12, 5),
218
- chart_type: str = "both",
219
- title: str | None = None,
220
- ) -> Figure:
221
- """Plot pie/bar chart of discovered message types.
222
-
223
- Args:
224
- message_types: List of MessageTypeInfo from RE analysis.
225
- figsize: Figure size (width, height) in inches.
226
- chart_type: Type of chart - "pie", "bar", or "both".
227
- title: Optional custom title.
228
-
229
- Returns:
230
- Matplotlib figure object.
231
-
232
- Example:
233
- >>> fig = plot_message_type_distribution(result.message_types)
234
- """
235
- if not message_types:
236
- fig, ax = plt.subplots(figsize=figsize)
237
- ax.text(0.5, 0.5, "No message types detected", ha="center", va="center", fontsize=14)
238
- ax.set_axis_off()
239
- return fig
240
-
241
- if chart_type == "both":
242
- fig, (ax1, ax2) = plt.subplots(1, 2, figsize=figsize)
243
- _plot_type_pie(ax1, message_types)
244
- _plot_type_bar(ax2, message_types)
245
- elif chart_type == "pie":
246
- fig, ax = plt.subplots(figsize=figsize)
247
- _plot_type_pie(ax, message_types)
248
- else: # bar
249
- fig, ax = plt.subplots(figsize=figsize)
250
- _plot_type_bar(ax, message_types)
251
-
252
- if title:
253
- fig.suptitle(title, fontsize=12, fontweight="bold")
254
- else:
255
- fig.suptitle("Message Type Distribution", fontsize=12, fontweight="bold")
256
-
257
- plt.tight_layout()
258
- return fig
259
-
260
-
261
- def plot_message_field_layout(
262
- schema: MessageSchema,
263
- *,
264
- figsize: tuple[float, float] = (14, 6),
265
- title: str | None = None,
266
- show_values: bool = True,
267
- ) -> Figure:
268
- """Create visual field layout diagram showing byte positions.
269
-
270
- Displays a horizontal layout of message fields with:
271
- - Field boundaries marked
272
- - Field types color-coded
273
- - Byte offsets labeled
274
- - Optional sample values
275
-
276
- Args:
277
- schema: MessageSchema with inferred field structure.
278
- figsize: Figure size (width, height) in inches.
279
- title: Optional custom title.
280
- show_values: Whether to show sample field values.
281
-
282
- Returns:
283
- Matplotlib figure object.
284
-
285
- Example:
286
- >>> from oscura.inference.message_format import infer_format
287
- >>> schema = infer_format(messages)
288
- >>> fig = plot_message_field_layout(schema)
289
- """
290
- fig, ax = plt.subplots(figsize=figsize)
291
-
292
- if not schema.fields:
293
- ax.text(0.5, 0.5, "No fields detected", ha="center", va="center", fontsize=14)
294
- ax.set_axis_off()
295
- return fig
296
-
297
- type_colors = _get_field_type_colors()
298
- total_bytes = schema.total_size
299
- bar_height = 0.6
300
- y_center = 0.5
301
-
302
- _draw_field_rectangles(ax, schema.fields, total_bytes, bar_height, y_center, type_colors)
303
- _add_field_offsets(ax, schema.fields, total_bytes, bar_height, y_center)
304
- _add_field_legend(ax, schema.fields, type_colors)
305
- _format_layout_axes(ax, schema.total_size, title)
306
-
307
- return fig
308
-
309
-
310
- def plot_field_confidence_heatmap(
311
- fields: list[InferredField],
312
- *,
313
- figsize: tuple[float, float] = (12, 6),
314
- title: str | None = None,
315
- ) -> Figure:
316
- """Create heatmap showing confidence scores for field inferences.
317
-
318
- Displays a visual representation of how confident the inference
319
- algorithm is about each field's type and boundaries.
320
-
321
- Args:
322
- fields: List of InferredField objects with confidence scores.
323
- figsize: Figure size (width, height) in inches.
324
- title: Optional custom title.
325
-
326
- Returns:
327
- Matplotlib figure object.
328
-
329
- Example:
330
- >>> fig = plot_field_confidence_heatmap(schema.fields)
331
- """
332
- fig, ax = plt.subplots(figsize=figsize)
333
-
334
- if not fields:
335
- ax.text(0.5, 0.5, "No fields to display", ha="center", va="center", fontsize=14)
336
- ax.set_axis_off()
337
- return fig
338
-
339
- # Create data matrix
340
- n_fields = len(fields)
341
- metrics = ["Confidence", "Entropy", "Variance"]
342
- data = np.zeros((len(metrics), n_fields))
343
-
344
- for i, field in enumerate(fields):
345
- data[0, i] = field.confidence
346
- # Normalize entropy (0-8 bits) to 0-1
347
- data[1, i] = min(field.entropy / 8.0, 1.0)
348
- # Normalize variance using log scale
349
- data[2, i] = min(np.log1p(field.variance) / 10.0, 1.0)
350
-
351
- # Create heatmap
352
- im = ax.imshow(data, cmap="RdYlGn", aspect="auto", vmin=0, vmax=1)
353
-
354
- # Add colorbar
355
- cbar = plt.colorbar(im, ax=ax)
356
- cbar.set_label("Score (normalized)", fontsize=10)
357
-
358
- # Set labels
359
- field_names = [f.name for f in fields]
360
- ax.set_xticks(range(n_fields))
361
- ax.set_xticklabels(field_names, rotation=45, ha="right", fontsize=9)
362
- ax.set_yticks(range(len(metrics)))
363
- ax.set_yticklabels(metrics, fontsize=10)
364
-
365
- # Add text annotations
366
- for i in range(len(metrics)):
367
- for j in range(n_fields):
368
- value = data[i, j]
369
- color = "white" if value < 0.5 else "black"
370
- ax.text(j, i, f"{value:.2f}", ha="center", va="center", color=color, fontsize=8)
371
-
372
- if title:
373
- ax.set_title(title, fontsize=12, fontweight="bold", pad=10)
374
- else:
375
- ax.set_title("Field Inference Confidence Heatmap", fontsize=12, fontweight="bold", pad=10)
376
-
377
- plt.tight_layout()
378
- return fig
379
-
380
-
381
- def plot_protocol_candidates(
382
- candidates: list[ProtocolCandidate],
383
- *,
384
- figsize: tuple[float, float] = (10, 6),
385
- title: str | None = None,
386
- top_n: int = 10,
387
- ) -> Figure:
388
- """Create bar chart of protocol detection scores.
389
-
390
- Shows the confidence scores for each detected protocol candidate,
391
- with visual indicators for the evidence sources.
392
-
393
- Args:
394
- candidates: List of ProtocolCandidate objects.
395
- figsize: Figure size (width, height) in inches.
396
- title: Optional custom title.
397
- top_n: Maximum number of candidates to display.
398
-
399
- Returns:
400
- Matplotlib figure object.
401
-
402
- Example:
403
- >>> fig = plot_protocol_candidates(result.protocol_candidates)
404
- """
405
- fig, ax = plt.subplots(figsize=figsize)
406
-
407
- if not candidates:
408
- ax.text(0.5, 0.5, "No protocol candidates detected", ha="center", va="center", fontsize=14)
409
- ax.set_axis_off()
410
- return fig
411
-
412
- # Sort by confidence and take top N
413
- sorted_candidates = sorted(candidates, key=lambda c: c.confidence, reverse=True)[:top_n]
414
-
415
- names = [c.name for c in sorted_candidates]
416
- confidences = [c.confidence for c in sorted_candidates]
417
-
418
- # Color based on confidence level
419
- colors = []
420
- for conf in confidences:
421
- if conf >= 0.8:
422
- colors.append("#4CAF50") # Green - high confidence
423
- elif conf >= 0.5:
424
- colors.append("#FF9800") # Orange - medium
425
- else:
426
- colors.append("#F44336") # Red - low
427
-
428
- y_pos = np.arange(len(names))
429
- ax.barh(y_pos, confidences, color=colors, edgecolor="black")
430
-
431
- # Add evidence indicators
432
- for i, cand in enumerate(sorted_candidates):
433
- indicators = []
434
- if cand.port_hint:
435
- indicators.append("P") # Port hint
436
- if cand.header_match:
437
- indicators.append("H") # Header match
438
- if cand.matched_patterns:
439
- indicators.append(f"M{len(cand.matched_patterns)}") # Pattern matches
440
-
441
- if indicators:
442
- ax.text(
443
- confidences[i] + 0.02,
444
- i,
445
- " ".join(indicators),
446
- va="center",
447
- fontsize=8,
448
- color="gray",
449
- )
450
-
451
- ax.set_yticks(y_pos)
452
- ax.set_yticklabels(names)
453
- ax.set_xlim(0, 1.15)
454
- ax.set_xlabel("Confidence Score")
455
- ax.axvline(x=0.5, color="gray", linestyle="--", alpha=0.5, label="Threshold")
456
-
457
- # Add legend for evidence indicators
458
- ax.text(
459
- 0.95,
460
- -0.12,
461
- "P=Port hint, H=Header match, M#=Pattern matches",
462
- transform=ax.transAxes,
463
- fontsize=8,
464
- ha="right",
465
- style="italic",
466
- color="gray",
467
- )
468
-
469
- if title:
470
- ax.set_title(title, fontsize=12, fontweight="bold")
471
- else:
472
- ax.set_title("Protocol Detection Candidates", fontsize=12, fontweight="bold")
473
-
474
- plt.tight_layout()
475
- return fig
476
-
477
-
478
- def plot_crc_parameters(
479
- params: CRCParameters,
480
- *,
481
- figsize: tuple[float, float] = (10, 6),
482
- title: str | None = None,
483
- ) -> Figure:
484
- """Create visualization of detected CRC parameters.
485
-
486
- Shows the recovered CRC parameters in a visually informative format,
487
- including polynomial representation, configuration flags, and
488
- confidence metrics.
489
-
490
- Args:
491
- params: CRCParameters object with recovered CRC settings.
492
- figsize: Figure size (width, height) in inches.
493
- title: Optional custom title.
494
-
495
- Returns:
496
- Matplotlib figure object.
497
-
498
- Example:
499
- >>> from oscura.inference.crc_reverse import CRCReverser
500
- >>> reverser = CRCReverser()
501
- >>> params = reverser.reverse(messages)
502
- >>> fig = plot_crc_parameters(params)
503
- """
504
- fig, (ax1, ax2) = plt.subplots(1, 2, figsize=figsize)
505
- _plot_crc_parameter_table(ax1, params)
506
- _plot_crc_confidence_gauge(ax2, params)
507
- _finalize_crc_plot(fig, title)
508
- return fig
509
-
510
-
511
- def _plot_crc_parameter_table(ax: Any, params: CRCParameters) -> None:
512
- """Plot CRC parameter table.
513
-
514
- Args:
515
- ax: Axes object.
516
- params: CRC parameters.
517
- """
518
- ax.axis("off")
519
- param_lines = [
520
- f"Width: {params.width} bits",
521
- f"Polynomial: 0x{params.polynomial:0{params.width // 4}X}",
522
- f"Init Value: 0x{params.init:0{params.width // 4}X}",
523
- f"XOR Out: 0x{params.xor_out:0{params.width // 4}X}",
524
- f"Reflect In: {'Yes' if params.reflect_in else 'No'}",
525
- f"Reflect Out: {'Yes' if params.reflect_out else 'No'}",
526
- ]
527
-
528
- if params.algorithm_name:
529
- param_lines.insert(0, f"Algorithm: {params.algorithm_name}")
530
-
531
- ax.text(0.5, 0.98, "CRC Parameters", ha="center", fontsize=14, fontweight="bold")
532
-
533
- y_start = 0.9
534
- y_step = 0.12
535
- for i, line in enumerate(param_lines):
536
- y = y_start - i * y_step
537
- ax.text(0.1, y, line, fontsize=11, fontfamily="monospace", va="top")
538
-
539
-
540
- def _plot_crc_confidence_gauge(ax: Any, params: CRCParameters) -> None:
541
- """Plot confidence gauge for CRC parameters.
542
-
543
- Args:
544
- ax: Axes object.
545
- params: CRC parameters.
546
- """
547
- ax.set_aspect("equal")
548
-
549
- # Background arc
550
- theta = np.linspace(0, np.pi, 100)
551
- r = 0.8
552
- x = r * np.cos(theta)
553
- y = r * np.sin(theta)
554
- ax.plot(x, y, color="#E0E0E0", linewidth=20, solid_capstyle="round")
555
-
556
- # Confidence arc (colored)
557
- conf_angle = np.pi * params.confidence
558
- theta_conf = np.linspace(0, conf_angle, int(100 * params.confidence) + 1)
559
- x_conf = r * np.cos(theta_conf)
560
- y_conf = r * np.sin(theta_conf)
561
-
562
- color = _get_confidence_color(params.confidence)
563
- ax.plot(x_conf, y_conf, color=color, linewidth=20, solid_capstyle="round")
564
-
565
- # Add text annotations
566
- ax.text(
567
- 0, 0.2, f"{params.confidence:.0%}", ha="center", va="center", fontsize=24, fontweight="bold"
568
- )
569
- ax.text(0, -0.1, "Confidence", ha="center", va="center", fontsize=12)
570
- ax.text(
571
- 0,
572
- -0.4,
573
- f"Test Pass Rate: {params.test_pass_rate:.0%}",
574
- ha="center",
575
- va="center",
576
- fontsize=10,
577
- )
578
-
579
- ax.set_xlim(-1.2, 1.2)
580
- ax.set_ylim(-0.6, 1.2)
581
- ax.axis("off")
582
-
583
-
584
- def _get_confidence_color(confidence: float) -> str:
585
- """Get color based on confidence level.
586
-
587
- Args:
588
- confidence: Confidence value (0-1).
589
-
590
- Returns:
591
- Hex color code.
592
- """
593
- if confidence >= 0.8:
594
- return "#4CAF50" # Green
595
- if confidence >= 0.5:
596
- return "#FF9800" # Orange
597
- return "#F44336" # Red
598
-
599
-
600
- def _finalize_crc_plot(fig: Figure, title: str | None) -> None:
601
- """Finalize CRC plot with title.
602
-
603
- Args:
604
- fig: Figure object.
605
- title: Optional title.
606
- """
607
- if title:
608
- fig.suptitle(title, fontsize=14, fontweight="bold")
609
- else:
610
- fig.suptitle("CRC Parameter Recovery", fontsize=14, fontweight="bold")
611
- plt.tight_layout()
612
-
613
-
614
- def plot_pipeline_timing(
615
- statistics: dict[str, Any],
616
- *,
617
- figsize: tuple[float, float] = (12, 6),
618
- title: str | None = None,
619
- ) -> Figure:
620
- """Create performance metrics visualization for pipeline stages.
621
-
622
- Shows timing breakdown for each stage of the RE pipeline,
623
- identifying bottlenecks and processing efficiency.
624
-
625
- Args:
626
- statistics: Statistics dictionary from REAnalysisResult.
627
- figsize: Figure size (width, height) in inches.
628
- title: Optional custom title.
629
-
630
- Returns:
631
- Matplotlib figure object.
632
-
633
- Example:
634
- >>> fig = plot_pipeline_timing(result.statistics)
635
- """
636
- fig, (ax1, ax2) = plt.subplots(1, 2, figsize=figsize)
637
-
638
- stage_timing = statistics.get("stage_timing", {})
639
-
640
- if not stage_timing:
641
- ax1.text(0.5, 0.5, "No timing data available", ha="center", va="center", fontsize=14)
642
- ax1.set_axis_off()
643
- ax2.set_axis_off()
644
- return fig
645
-
646
- stages = list(stage_timing.keys())
647
- times = list(stage_timing.values())
648
- total_time = sum(times)
649
-
650
- # Left panel: Bar chart of stage times
651
- colors = plt.cm.viridis(np.linspace(0.2, 0.8, len(stages)))
652
- y_pos = np.arange(len(stages))
653
-
654
- bars = ax1.barh(y_pos, times, color=colors)
655
- ax1.set_yticks(y_pos)
656
- ax1.set_yticklabels([s.replace("_", " ").title() for s in stages])
657
- ax1.set_xlabel("Time (seconds)")
658
-
659
- # Add time labels on bars
660
- for i, (_bar, t) in enumerate(zip(bars, times, strict=False)):
661
- pct = t / total_time * 100 if total_time > 0 else 0
662
- ax1.text(t + 0.01, i, f"{t:.3f}s ({pct:.1f}%)", va="center", fontsize=9)
663
-
664
- ax1.set_title("Stage Execution Time", fontsize=11, fontweight="bold")
665
-
666
- # Right panel: Pie chart of time distribution
667
- # Filter out very small stages for cleaner pie
668
- significant = [(s, t) for s, t in zip(stages, times, strict=False) if t / total_time > 0.02]
669
- if significant:
670
- pie_stages_tuple, pie_times_tuple = zip(*significant, strict=False)
671
- other_time = total_time - sum(pie_times_tuple)
672
- pie_stages: list[str] = list(pie_stages_tuple)
673
- pie_times: list[float] = list(pie_times_tuple)
674
- if other_time > 0:
675
- pie_stages = pie_stages + ["Other"]
676
- pie_times = pie_times + [other_time]
677
-
678
- wedges, texts, autotexts = ax2.pie(
679
- pie_times,
680
- labels=[s.replace("_", " ").title() for s in pie_stages],
681
- autopct="%1.1f%%",
682
- colors=plt.cm.viridis(np.linspace(0.2, 0.8, len(pie_stages))),
683
- )
684
- ax2.set_title("Time Distribution", fontsize=11, fontweight="bold")
685
- else:
686
- ax2.text(0.5, 0.5, "Insufficient data", ha="center", va="center")
687
- ax2.set_axis_off()
688
-
689
- # Add total time annotation
690
- fig.text(
691
- 0.5,
692
- 0.02,
693
- f"Total Pipeline Time: {total_time:.3f} seconds",
694
- ha="center",
695
- fontsize=10,
696
- style="italic",
697
- )
698
-
699
- if title:
700
- fig.suptitle(title, fontsize=12, fontweight="bold")
701
- else:
702
- fig.suptitle("Pipeline Performance Metrics", fontsize=12, fontweight="bold")
703
-
704
- plt.tight_layout(rect=(0, 0.05, 1, 0.95))
705
- return fig
706
-
707
-
708
- # ============================================================================
709
- # Helper functions for panel plotting
710
- # ============================================================================
711
-
712
-
713
- def _plot_summary_stats(ax: Any, result: REAnalysisResult) -> None:
714
- """Plot summary statistics panel."""
715
- ax.axis("off")
716
-
717
- stats = [
718
- ("Flows Analyzed", str(result.flow_count)),
719
- ("Messages", str(result.message_count)),
720
- ("Message Types", str(len(result.message_types))),
721
- ("Protocol Candidates", str(len(result.protocol_candidates))),
722
- ("Field Schemas", str(len(result.field_schemas))),
723
- ("Duration", f"{result.duration_seconds:.2f}s"),
724
- ("Warnings", str(len(result.warnings))),
725
- ]
726
-
727
- ax.text(0.5, 0.95, "Analysis Summary", ha="center", fontsize=11, fontweight="bold")
728
-
729
- y_pos = 0.8
730
- for label, value in stats:
731
- ax.text(0.1, y_pos, f"{label}:", fontsize=9, fontweight="bold")
732
- ax.text(0.7, y_pos, value, fontsize=9, ha="right")
733
- y_pos -= 0.11
734
-
735
-
736
- def _plot_message_types_panel(ax: Any, message_types: list[MessageTypeInfo]) -> None:
737
- """Plot message types panel."""
738
- if not message_types:
739
- ax.text(0.5, 0.5, "No types", ha="center", va="center")
740
- ax.set_title("Message Types", fontsize=10)
741
- return
742
-
743
- names = [mt.name[:15] for mt in message_types[:8]]
744
- counts = [mt.sample_count for mt in message_types[:8]]
745
-
746
- colors = plt.cm.Set3(np.linspace(0, 1, len(names)))
747
- ax.pie(counts, labels=names, colors=colors, autopct="%1.0f%%", textprops={"fontsize": 8})
748
- ax.set_title("Message Types", fontsize=10)
749
-
750
-
751
- def _plot_protocol_panel(ax: Any, candidates: list[ProtocolCandidate]) -> None:
752
- """Plot protocol candidates panel."""
753
- if not candidates:
754
- ax.text(0.5, 0.5, "No candidates", ha="center", va="center")
755
- ax.set_title("Protocol Candidates", fontsize=10)
756
- return
757
-
758
- sorted_cand = sorted(candidates, key=lambda c: c.confidence, reverse=True)[:5]
759
- names = [c.name for c in sorted_cand]
760
- confs = [c.confidence for c in sorted_cand]
761
-
762
- y_pos = np.arange(len(names))
763
- colors = ["#4CAF50" if c >= 0.7 else "#FF9800" if c >= 0.4 else "#F44336" for c in confs]
764
-
765
- ax.barh(y_pos, confs, color=colors)
766
- ax.set_yticks(y_pos)
767
- ax.set_yticklabels(names, fontsize=8)
768
- ax.set_xlim(0, 1)
769
- ax.set_title("Protocol Candidates", fontsize=10)
770
-
771
-
772
- def _plot_timing_panel(ax: Any, statistics: dict[str, Any]) -> None:
773
- """Plot timing panel."""
774
- stage_timing = statistics.get("stage_timing", {})
775
-
776
- if not stage_timing:
777
- ax.text(0.5, 0.5, "No timing data", ha="center", va="center")
778
- ax.set_title("Stage Timing", fontsize=10)
779
- return
780
-
781
- stages = list(stage_timing.keys())
782
- times = list(stage_timing.values())
783
-
784
- colors = plt.cm.viridis(np.linspace(0.2, 0.8, len(stages)))
785
- bars = ax.bar(range(len(stages)), times, color=colors)
786
-
787
- ax.set_xticks(range(len(stages)))
788
- ax.set_xticklabels([s.replace("_", "\n") for s in stages], fontsize=8)
789
- ax.set_ylabel("Time (s)")
790
- ax.set_title("Pipeline Stage Timing", fontsize=10)
791
-
792
- for bar, t in zip(bars, times, strict=False):
793
- ax.text(
794
- bar.get_x() + bar.get_width() / 2,
795
- bar.get_height(),
796
- f"{t:.2f}s",
797
- ha="center",
798
- va="bottom",
799
- fontsize=7,
800
- )
801
-
802
-
803
- def _plot_warnings_panel(ax: Any, result: REAnalysisResult) -> None:
804
- """Plot warnings/info panel."""
805
- ax.axis("off")
806
-
807
- ax.text(0.5, 0.95, "Status & Warnings", ha="center", fontsize=10, fontweight="bold")
808
-
809
- if result.warnings:
810
- y_pos = 0.8
811
- for warning in result.warnings[:5]:
812
- ax.text(0.05, y_pos, f"! {warning[:40]}...", fontsize=8, color="orange")
813
- y_pos -= 0.15
814
- else:
815
- ax.text(0.5, 0.5, "No warnings", ha="center", va="center", fontsize=10, color="green")
816
-
817
-
818
- def _plot_type_pie(ax: Any, message_types: list[MessageTypeInfo]) -> None:
819
- """Plot message type pie chart."""
820
- names = [mt.name for mt in message_types]
821
- counts = [mt.sample_count for mt in message_types]
822
-
823
- colors = plt.cm.Set3(np.linspace(0, 1, len(names)))
824
- ax.pie(counts, labels=names, colors=colors, autopct="%1.1f%%")
825
- ax.set_title("Distribution by Sample Count")
826
-
827
-
828
- def _plot_type_bar(ax: Any, message_types: list[MessageTypeInfo]) -> None:
829
- """Plot message type bar chart."""
830
- names = [mt.name for mt in message_types]
831
- counts = [mt.sample_count for mt in message_types]
832
-
833
- colors = plt.cm.viridis(np.linspace(0.2, 0.8, len(names)))
834
- ax.bar(range(len(names)), counts, color=colors)
835
- ax.set_xticks(range(len(names)))
836
- ax.set_xticklabels(names, rotation=45, ha="right")
837
- ax.set_ylabel("Sample Count")
838
- ax.set_title("Message Count per Type")