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,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")