vanken 0.1.0 → 0.3.0

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 (105) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +24 -0
  3. data/README.md +28 -11
  4. data/data/coloring_rules.yml +67 -0
  5. data/docs/performance.md +53 -0
  6. data/docs/releases.md +2 -4
  7. data/docs/usage.md +51 -0
  8. data/exe/vanken-setup-permissions +6 -0
  9. data/lib/vanken/app/capture_controller.rb +66 -1
  10. data/lib/vanken/app/document.rb +102 -39
  11. data/lib/vanken/app/document_analysis.rb +75 -0
  12. data/lib/vanken/app/document_jobs.rb +52 -9
  13. data/lib/vanken/app/expert_info.rb +26 -0
  14. data/lib/vanken/app/io_graph.rb +47 -0
  15. data/lib/vanken/app/navigation.rb +77 -0
  16. data/lib/vanken/app/search_job.rb +62 -0
  17. data/lib/vanken/app/stats_job.rb +45 -0
  18. data/lib/vanken/capture/analyzer.rb +3 -1
  19. data/lib/vanken/capture/analyzer_process.rb +9 -6
  20. data/lib/vanken/capture/analyzer_worker.rb +2 -2
  21. data/lib/vanken/capture/filter_worker.rb +8 -2
  22. data/lib/vanken/capture/helper_main.rb +10 -3
  23. data/lib/vanken/capture/helper_options.rb +6 -2
  24. data/lib/vanken/capture/launcher.rb +1 -0
  25. data/lib/vanken/capture/permission_setup.rb +201 -0
  26. data/lib/vanken/config/analysis_settings.rb +58 -0
  27. data/lib/vanken/config/columns.rb +138 -0
  28. data/lib/vanken/config/messages.rb +105 -0
  29. data/lib/vanken/config/preferences.rb +34 -8
  30. data/lib/vanken/config/profiles.rb +74 -0
  31. data/lib/vanken/config/sessions.rb +69 -0
  32. data/lib/vanken/config/yaml_file.rb +36 -0
  33. data/lib/vanken/core/coloring.rb +86 -0
  34. data/lib/vanken/core/frame_store.rb +2 -2
  35. data/lib/vanken/core/stores.rb +12 -0
  36. data/lib/vanken/gateway/detail_builder.rb +1 -1
  37. data/lib/vanken/gateway/display_capture_filter.rb +102 -0
  38. data/lib/vanken/gateway/dissector.rb +31 -6
  39. data/lib/vanken/gateway/exporter.rb +125 -0
  40. data/lib/vanken/gateway/file_writer.rb +26 -2
  41. data/lib/vanken/gateway/interfaces.rb +59 -0
  42. data/lib/vanken/gateway/resolver.rb +131 -0
  43. data/lib/vanken/gateway/statistics.rb +83 -0
  44. data/lib/vanken/gateway/stream.rb +159 -0
  45. data/lib/vanken/ui/actions.rb +54 -28
  46. data/lib/vanken/ui/analysis_dialogs.rb +371 -0
  47. data/lib/vanken/ui/application.rb +183 -9
  48. data/lib/vanken/ui/coloring_operations.rb +104 -0
  49. data/lib/vanken/ui/column_operations.rb +115 -0
  50. data/lib/vanken/ui/dialogs.rb +41 -26
  51. data/lib/vanken/ui/file_operations.rb +17 -12
  52. data/lib/vanken/ui/filter_operations.rb +43 -15
  53. data/lib/vanken/ui/main_view.rb +74 -29
  54. data/lib/vanken/ui/navigation_operations.rb +86 -0
  55. data/lib/vanken/ui/packet_source.rb +33 -1
  56. data/lib/vanken/ui/selection.rb +8 -2
  57. data/lib/vanken/ui/settings_operations.rb +233 -0
  58. data/lib/vanken/version.rb +1 -1
  59. data/packaging/README.md +41 -1
  60. data/packaging/linux/org.vanken.capture.policy +14 -0
  61. data/packaging/linux/vanken-capture.wrapper +6 -0
  62. data/packaging/linux/vanken.desktop +9 -0
  63. data/packaging/linux/vanken.sudoers +2 -0
  64. data/packaging/macos/chmod-bpf +6 -0
  65. data/packaging/macos/org.vanken.chmod-bpf.plist +8 -0
  66. data/sig/generated/vanken/app/capture_controller.rbs +16 -0
  67. data/sig/generated/vanken/app/document.rbs +23 -1
  68. data/sig/generated/vanken/app/document_analysis.rbs +21 -0
  69. data/sig/generated/vanken/app/document_jobs.rbs +6 -0
  70. data/sig/generated/vanken/app/expert_info.rbs +30 -0
  71. data/sig/generated/vanken/app/io_graph.rbs +45 -0
  72. data/sig/generated/vanken/app/navigation.rbs +31 -0
  73. data/sig/generated/vanken/app/search_job.rbs +21 -0
  74. data/sig/generated/vanken/app/stats_job.rbs +18 -0
  75. data/sig/generated/vanken/capture/helper_options.rbs +6 -0
  76. data/sig/generated/vanken/capture/permission_setup.rbs +43 -0
  77. data/sig/generated/vanken/config/analysis_settings.rbs +31 -0
  78. data/sig/generated/vanken/config/columns.rbs +37 -0
  79. data/sig/generated/vanken/config/messages.rbs +14 -0
  80. data/sig/generated/vanken/config/preferences.rbs +1 -1
  81. data/sig/generated/vanken/config/profiles.rbs +29 -0
  82. data/sig/generated/vanken/config/sessions.rbs +17 -0
  83. data/sig/generated/vanken/config/yaml_file.rbs +11 -0
  84. data/sig/generated/vanken/core/coloring.rbs +52 -0
  85. data/sig/generated/vanken/core/stores.rbs +4 -0
  86. data/sig/generated/vanken/gateway/display_capture_filter.rbs +37 -0
  87. data/sig/generated/vanken/gateway/dissector.rbs +15 -2
  88. data/sig/generated/vanken/gateway/exporter.rbs +27 -0
  89. data/sig/generated/vanken/gateway/file_writer.rbs +3 -0
  90. data/sig/generated/vanken/gateway/interfaces.rbs +12 -0
  91. data/sig/generated/vanken/gateway/resolver.rbs +52 -0
  92. data/sig/generated/vanken/gateway/statistics.rbs +104 -0
  93. data/sig/generated/vanken/gateway/stream.rbs +75 -0
  94. data/sig/generated/vanken/ui/analysis_dialogs.rbs +71 -0
  95. data/sig/generated/vanken/ui/application.rbs +23 -1
  96. data/sig/generated/vanken/ui/coloring_operations.rbs +29 -0
  97. data/sig/generated/vanken/ui/column_operations.rbs +29 -0
  98. data/sig/generated/vanken/ui/dialogs.rbs +5 -3
  99. data/sig/generated/vanken/ui/file_operations.rbs +2 -0
  100. data/sig/generated/vanken/ui/filter_operations.rbs +3 -1
  101. data/sig/generated/vanken/ui/main_view.rbs +14 -2
  102. data/sig/generated/vanken/ui/navigation_operations.rbs +43 -0
  103. data/sig/generated/vanken/ui/packet_source.rbs +4 -0
  104. data/sig/generated/vanken/ui/settings_operations.rbs +41 -0
  105. metadata +61 -3
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ module Vanken
5
+ module Config
6
+ module Messages
7
+ EN = {
8
+ "TCP ストリームを追跡" => "Follow TCP Stream", "別プロトコルとして解析" => "Decode As",
9
+ "注記" => "note", "警告" => "warning", "エラー" => "error", "トラフィック" => "traffic",
10
+ "開く" => "Open", "名前を付けて保存" => "Save as", "ファイルを閉じる" => "Close file", "再読み込み" => "Reload",
11
+ "終了" => "Quit", "設定" => "Preferences", "キャプチャ開始/停止" => "Start or stop capture", "キャプチャ再開" => "Restart capture",
12
+ "フィルタを適用" => "Apply filter", "フィルタをクリア" => "Clear filter", "フィルタ履歴" => "Filter history", "フィルタを保存" => "Save filter",
13
+ "選択した項目でフィルタ" => "Filter by selected field", "選択した項目のフィルタを準備" => "Prepare filter from selected field",
14
+ "表示する列" => "Visible columns", "末尾追従" => "Follow tail", "末尾追従 ✓" => "Follow tail ✓",
15
+ "拡大" => "Zoom in", "縮小" => "Zoom out", "標準サイズ" => "Reset zoom",
16
+ "前のパケット" => "Previous packet", "次のパケット" => "Next packet", "最初のパケット" => "First packet", "最後のパケット" => "Last packet",
17
+ "パケットへ移動" => "Go to packet", "項目をフィルタとしてコピー" => "Copy field as filter",
18
+ "パケット検索" => "Find packet", "次を検索" => "Find next", "前を検索" => "Find previous",
19
+ "マーク/解除" => "Mark or unmark packet", "表示中をすべてマーク" => "Mark all displayed packets", "すべてのマークを解除" => "Unmark all packets",
20
+ "無視/解除" => "Ignore or unignore packet", "時刻基準/解除" => "Set or unset time reference", "同一会話の前" => "Previous in conversation", "同一会話の次" => "Next in conversation",
21
+ "色付けルール" => "Coloring rules", "色付け" => "Packet coloring", "エキスパート情報" => "Expert information",
22
+ "プラグイン再読み込み" => "Reload plugins", "ディセクタプラグイン" => "Dissector plugins", "キャプチャファイルのプロパティ" => "Capture file properties",
23
+ "プロトコル階層" => "Protocol hierarchy", "会話" => "Conversations", "端点" => "Endpoints", "I/O グラフ" => "I/O graph",
24
+ "指定パケットのエクスポート" => "Export specified packets", "解析結果のエクスポート" => "Export dissections", "コマンドパレット" => "Command palette", "プロファイル" => "Profiles",
25
+ "時刻: %{name}" => "Time: %{name}", "テーマ: %{name}" => "Theme: %{name}", "選択項目" => "Selected field", "選択項目を除外" => "Exclude selected field",
26
+ "適用: %{label}" => "Apply: %{label}", "準備: %{label}" => "Prepare: %{label}", "16 進数" => "Hexadecimal", "テキスト" => "Text", "ダンプ" => "Hex dump",
27
+ "エスケープした文字列" => "Escaped text", "バイトをコピー: %{label}" => "Copy bytes: %{label}",
28
+ "ファイル" => "File", "編集" => "Edit", "表示" => "View", "時刻形式" => "Time format", "テーマ" => "Theme", "移動" => "Go",
29
+ "キャプチャ" => "Capture", "フィルタ" => "Filter", "選択項目を適用" => "Apply selected field", "選択項目から準備" => "Prepare from selected field",
30
+ "分析" => "Analyze", "統計" => "Statistics", "ヘルプ" => "Help", "表示中のパケットのみ" => "Displayed packets only",
31
+ "%{value}秒" => "%{value} s", "間隔" => "Interval", "系列の表示フィルタ(1行に1系列、空行は全件)" => "Series display filters (one per line; blank means all packets)",
32
+ "系列を適用" => "Apply series", "表示するパケットがありません。" => "There are no packets to display.", "全件 %{index}" => "All packets %{index}",
33
+ "先頭の%{count}区間を省略しています。間隔を広げると全期間を表示できます。" => "The first %{count} intervals are omitted. Select a wider interval to show the entire capture.",
34
+ "TCPストリームを持つパケットを選択してください。" => "Select a packet with a TCP stream.", "ストリーム番号" => "Stream number",
35
+ "16進ダンプ" => "Hex dump", "生データ(保存用)" => "Raw data (for saving)", "表示形式" => "Display format", "両方向" => "Both directions",
36
+ "クライアント → サーバー" => "Client → server", "サーバー → クライアント" => "Server → client", "方向" => "Direction",
37
+ "ストリーム番号は0以上の整数で指定してください。" => "The stream number must be a nonnegative integer.", "文字列検索" => "Find text",
38
+ "保存" => "Save", "このストリームを除外" => "Exclude this stream", "表示は16 MiBまでです。保存は全量を含みます。" => "The display is limited to 16 MiB. Saving includes the entire stream.",
39
+ "範囲(1-10,20,30-)" => "Range (1-10,20,30-)", "無視したパケットを除外" => "Exclude ignored packets", "全件" => "All packets", "表示中" => "Displayed",
40
+ "選択" => "Selected", "マーク済み" => "Marked", "最初と最後のマークの間" => "Between first and last marks", "範囲指定" => "Packet range", "対象" => "Scope",
41
+ "形式" => "Format", "エクスポート" => "Export", "解析結果" => "Dissections", "パケット" => "Packets", "%{kind}のエクスポート" => "Export %{kind}",
42
+ "SHA-256を計算" => "Calculate SHA-256", "集計中…" => "Calculating…", "メモリ上限により%{count}行が省略されました。" => "%{count} rows were omitted because of the memory limit.",
43
+ "フィルタとして適用" => "Apply as filter", "エクスポート先" => "Export destination", "未記録" => "Not recorded", "未保存" => "Unsaved",
44
+ "期間(秒)" => "Duration (seconds)", "合計(bytes)" => "Total bytes", "保存済み(bytes)" => "Captured bytes", "平均pps" => "Average packets/s", "平均bps" => "Average bits/s",
45
+ "インタフェース %{name}" => "Interface %{name}", "取得統計 %{key}" => "Capture statistics %{key}",
46
+ "検索内容" => "Search query", "表示フィルタ" => "Display filter", "16 進" => "Hex", "文字列" => "String", "正規表現" => "Regular expression",
47
+ "パケット一覧" => "Packet list", "パケット詳細" => "Packet details", "パケットバイト" => "Packet bytes", "検索対象" => "Search in", "キャンセル" => "Cancel",
48
+ "一致するパケットはありません。" => "No matching packets were found.", "ルール" => "Rule", "名前" => "Name", "有効" => "Enabled",
49
+ "ライト" => "Light", "ダーク" => "Dark", "文字" => "Text color", "背景" => "Background", "削除" => "Delete", "追加" => "Add", "新しいルール" => "New rule",
50
+ "%{theme} %{part}" => "%{theme} %{part}", "インポート" => "Import", "適用" => "Apply", "色付けルールの読み込み先" => "Import coloring rules", "色付けルールの保存先" => "Export coloring rules",
51
+ "インタフェース" => "Interface", "キャプチャフィルタ (BPF)" => "Capture filter (BPF)", "バッファサイズ (bytes)" => "Buffer size (bytes)",
52
+ "停止件数 (0: 無制限)" => "Stop after packet count (0: unlimited)", "停止までの秒数 (0: 無制限)" => "Stop after seconds (0: unlimited)", "停止サイズ (bytes、0: 無制限)" => "Stop after bytes (0: unlimited)",
53
+ "リング保存先 (空欄: 保存しない)" => "Ring file path (blank: disabled)", "リングファイル上限 (bytes)" => "Ring file size limit (bytes)", "リング切替間隔 (秒)" => "Ring rotation interval (seconds)", "リングファイル数" => "Ring file count",
54
+ "プロミスキャスモード" => "Promiscuous mode", "送受信" => "Inbound and outbound", "受信" => "Inbound", "送信" => "Outbound",
55
+ "インタフェースを取得中です。" => "Loading interfaces…", "GUI は通常ユーザーで動作し、取得ヘルパーに必要な権限を渡します。" => "The application runs as your user and grants the capture helper the required permissions.",
56
+ "開始" => "Start", "停止" => "Stop", "再開" => "Restart", "キャプチャオプション" => "Capture options", "時刻表示" => "Time display", "文字を小さく" => "Smaller text", "文字を大きく" => "Larger text",
57
+ "ファイルを開く" => "Open file", "キャプチャを停止してから操作してください。" => "Stop the capture before continuing.", "キャプチャ中" => "Capturing",
58
+ "このキャプチャはまだ保存されていません。" => "This capture has not been saved.", "破棄" => "Discard", "キャプチャを保存しますか?" => "Save this capture?",
59
+ "指定したパケットは表示されていません" => "The specified packet is not displayed", "%{message} (位置 %{position})" => "%{message} (position %{position})",
60
+ "フィルタとしてコピー" => "Copy as filter", "フィルタを準備" => "Prepare filter", "フィルタの名前" => "Filter name",
61
+ "プロファイルの管理" => "Manage profiles", "複製" => "Duplicate", "新しいプロファイル名" => "New profile name", "新規作成" => "Create profile", "例: udp.port==8443,dns" => "Example: udp.port==8443,dns",
62
+ "Ruby ディセクタのパス" => "Ruby dissector path", "Ruby プラグインはユーザー権限で任意のコードを実行します。信頼するファイルだけを読み込んでください。\n%{paths}" => "Ruby plugins can execute arbitrary code as your user. Load only files you trust.\n%{paths}",
63
+ "信頼して読み込む" => "Trust and load", "プラグインの確認" => "Confirm plugins", "コマンドを検索" => "Search commands", "解析" => "Analysis", "名前解決" => "Name resolution",
64
+ "キャプチャファイルを開くか、インタフェースを選んで開始してください" => "Open a capture file or select an interface to start capturing.",
65
+ "読み込み中" => "Loading", "%{state} %{count} / %{total} 表示 %{displayed} ドロップ %{dropped}" => "%{state} %{count} / %{total} Displayed %{displayed} Dropped %{dropped}",
66
+ "操作を完了できません" => "The operation could not be completed", "読み込みを中止" => "Cancel loading", "クリア" => "Clear", "履歴" => "History",
67
+ "パケットキャプチャを開くか、インタフェースを選択してキャプチャを開始します。" => "Open a packet capture or select an interface to start capturing.",
68
+ "キャプチャファイルを開く" => "Open capture file", "インタフェースを選択" => "Select interface",
69
+ "上へ移動" => "Move up", "下へ移動" => "Move down", "項目名" => "Column name", "表示フィールド" => "Display field", "列幅" => "Column width",
70
+ "列として適用" => "Apply as column", "カスタム列を追加" => "Add custom column", "カスタム列" => "Custom columns", "列の名前" => "Column label", "フィールド名" => "Field name",
71
+ "時刻" => "Time", "送信元" => "Source", "宛先" => "Destination", "プロトコル" => "Protocol", "長さ" => "Length", "情報" => "Info",
72
+ "重大度" => "Severity", "コード" => "Code", "件数" => "Count", "メッセージ" => "Message", "アドレス A" => "Address A", "ポート A" => "Port A", "アドレス B" => "Address B", "ポート B" => "Port B",
73
+ "パケット A → B" => "Packets A → B", "パケット B → A" => "Packets B → A", "バイト A → B" => "Bytes A → B", "バイト B → A" => "Bytes B → A",
74
+ "アドレス" => "Address", "ポート" => "Port", "送信パケット" => "Transmitted packets", "受信パケット" => "Received packets", "送信バイト" => "Transmitted bytes", "受信バイト" => "Received bytes", "期間(ns)" => "Duration (ns)",
75
+ "言語" => "Language", "文字サイズ" => "Font size", "時刻の精度" => "Time precision", "自動スクロール" => "Autoscroll", "行キャッシュ容量" => "Row cache size",
76
+ "チェックサム検証" => "Verify checksums", "解析メモリ上限(MiB)" => "Analysis memory limit (MiB)", "フロー数上限" => "Maximum flows", "ワーカー数" => "Workers",
77
+ "キャプチャ方式" => "Capture backend", "ヘルパー起動方式" => "Helper launcher", "解決タイムアウト(秒)" => "Resolution timeout (seconds)", "名前キャッシュ容量" => "Name cache size",
78
+ "保存せずに閉じる" => "Close without saving", "復旧可能なセッション" => "Recoverable sessions", "復旧" => "Recover", "一時セッションを復旧しますか?" => "Recover temporary sessions?",
79
+ "相対時刻" => "Relative time", "絶対時刻" => "Absolute time", "前パケットとの差" => "Delta from previous packet", "前表示パケットとの差" => "Delta from previous displayed packet", "エポック時刻" => "Epoch time",
80
+ "システム設定" => "System", "ハイコントラスト" => "High contrast", "ミリ秒" => "Milliseconds", "マイクロ秒" => "Microseconds", "ナノ秒" => "Nanoseconds",
81
+ "前回のキャプチャを復旧" => "Recover previous capture",
82
+ "%{label} %{count} / %{total} 表示 %{displayed} ドロップ %{dropped}%{progress}" => "%{label} %{count} / %{total} Displayed %{displayed} Dropped %{dropped}%{progress}",
83
+ "幅" => "Width", "列名 (空欄: フィールド名)" => "Column label (blank: field name)", "幅は40から4096の範囲で指定してください" => "Width must be between 40 and 4096",
84
+ "%{protocol} %{packets} パケット %{bytes} bytes %{percent}%" => "%{protocol} %{packets} packets %{bytes} bytes %{percent}%",
85
+ "区間ごとのパケット数" => "Packets per interval", "%{count} パケット" => "%{count} packets", "キャプチャファイル" => "Capture files", "閉じる" => "Close",
86
+ "取得バッファ (bytes)" => "Capture buffer (bytes)", "取得方向" => "Capture direction",
87
+ "自動停止: パケット数 (0: 無効)" => "Stop after packet count (0: disabled)", "自動停止: 秒 (0: 無効)" => "Stop after seconds (0: disabled)", "自動停止: bytes (0: 無効)" => "Stop after bytes (0: disabled)",
88
+ "リング保存先 (空欄: 無効)" => "Ring file path (blank: disabled)", "リング: ファイルごとのbytes" => "Ring: bytes per file", "リング: 秒" => "Ring: seconds", "リング: 保持ファイル数" => "Ring: retained files",
89
+ "検索形式" => "Search mode", "キャプチャ操作" => "Capture controls", "日本語" => "Japanese", "英語" => "English", "自動" => "Automatic", "直接" => "Direct",
90
+ "マーク" => "Mark", "マークを解除" => "Unmark", "無視" => "Ignore", "無視を解除" => "Unignore", "時刻基準にする" => "Set time reference", "時刻基準を解除" => "Unset time reference", "双方向" => "Both directions", "フィルタ履歴はまだありません。" => "There is no filter history yet."
91
+ }.freeze #: Hash[String, String]
92
+ JA = EN.invert.freeze #: Hash[String, String]
93
+
94
+ # @rbs (String text, ?language: String, **untyped values) -> String
95
+ def self.translate(text, language: "ja", **values)
96
+ dictionary = case language
97
+ when "ja" then JA
98
+ when "en" then EN
99
+ else raise Vanken::ConfigError, "invalid language: #{language}"
100
+ end
101
+ dictionary.fetch(text, text).gsub(/%\{(\w+)\}/) { values.fetch(Regexp.last_match(1).to_sym).to_s }
102
+ end
103
+ end
104
+ end
105
+ end
@@ -5,6 +5,7 @@ require "yaml"
5
5
  require "fileutils"
6
6
  require "logger"
7
7
  require "tempfile"
8
+ require_relative "yaml_file"
8
9
 
9
10
  module Vanken
10
11
  module Config
@@ -25,22 +26,32 @@ module Vanken
25
26
  class Preferences
26
27
  DEFAULTS = {
27
28
  "schema_version" => 1,
28
- "appearance" => {"theme" => "system", "font_size" => 13, "language" => "ja"},
29
- "packet_list" => {"time_format" => "relative", "time_precision" => "micro", "autoscroll" => true, "row_cache_rows" => 20_000},
30
- "capture" => {"snaplen" => 262_144, "promiscuous" => true, "buffer_size" => 4 << 20, "backend" => "auto", "launcher" => "auto", "direction" => "inout"},
29
+ "appearance" => {"theme" => "system", "font_size" => 13, "language" => ENV.fetch("LANG", "en").start_with?("ja") ? "ja" : "en"},
30
+ "packet_list" => {"time_format" => "relative", "time_precision" => "micro", "autoscroll" => true, "row_cache_rows" => 20_000, "coloring" => true},
31
+ "capture" => {"snaplen" => 262_144, "promiscuous" => true, "buffer_size" => 4 << 20, "backend" => "auto", "launcher" => "auto", "direction" => "inout",
32
+ "stop_count" => 0, "stop_duration" => 0.0, "stop_bytes" => 0, "ring_path" => "", "ring_max_bytes" => 0, "ring_interval" => 0.0, "ring_file_count" => 10},
33
+ "name_resolution" => {"enabled" => false, "timeout" => 1.0, "cache_size" => 256},
31
34
  "analysis" => {"verify_checksums" => false, "max_state_mib" => 256, "max_flows" => 100_000, "workers" => 4},
32
35
  "history" => [], "recent_files" => [], "bookmarks" => {}, "layout" => {"ratios" => [0.55, 0.6], "width" => 1280, "height" => 800, "columns" => []}
33
36
  }.freeze
34
37
  attr_reader :warning, :directory
35
38
 
36
- def initialize(directory: Paths.config)
39
+ def initialize(directory: Paths.config, state_directory: nil)
37
40
  @directory = directory
41
+ @state_directory = state_directory || (directory == Paths.config ? Paths.state : File.join(directory, "state"))
38
42
  @values = Marshal.load(Marshal.dump(DEFAULTS))
39
43
  path = File.join(directory, "preferences.yml")
40
- return unless File.file?(path)
41
- saved = YAML.safe_load(File.read(path), permitted_classes: [], aliases: false)
42
- raise Vanken::ConfigError, "unsupported preferences schema" unless saved.is_a?(Hash) && saved["schema_version"] == 1
43
- merge(@values, saved)
44
+ if File.file?(path)
45
+ saved = YAML.safe_load(File.read(path), permitted_classes: [], aliases: false)
46
+ raise Vanken::ConfigError, "unsupported preferences schema" unless saved.is_a?(Hash) && saved["schema_version"] == 1
47
+ merge(@values, saved)
48
+ end
49
+ state = YamlFile.read(File.join(@state_directory, "recent.yml"))
50
+ merge(@values, state.slice("history", "recent_files"))
51
+ window = YamlFile.read(File.join(@state_directory, "window_state.yml"))
52
+ merge(@values["layout"], window.slice("width", "height", "ratios"))
53
+ filters = YamlFile.read(File.join(@directory, "filters.yml"))
54
+ merge(@values, filters.slice("bookmarks"))
44
55
  validate
45
56
  rescue Psych::Exception, Vanken::ConfigError, SystemCallError, TypeError => error
46
57
  @values = Marshal.load(Marshal.dump(DEFAULTS))
@@ -78,6 +89,9 @@ module Vanken
78
89
  temporary.fsync
79
90
  temporary.close
80
91
  File.rename(temporary.path, File.join(@directory, "preferences.yml"))
92
+ YamlFile.write(File.join(@state_directory, "recent.yml"), @values.slice("history", "recent_files"))
93
+ YamlFile.write(File.join(@state_directory, "window_state.yml"), @values["layout"].slice("width", "height", "ratios"))
94
+ YamlFile.write(File.join(@directory, "filters.yml"), @values.slice("bookmarks"))
81
95
  ensure
82
96
  temporary&.close unless temporary&.closed?
83
97
  File.unlink(temporary.path) if temporary && File.exist?(temporary.path)
@@ -99,6 +113,7 @@ module Vanken
99
113
  validate_shape(DEFAULTS, @values)
100
114
  raise Vanken::ConfigError, "invalid theme" unless %w[system dark light high_contrast].include?(get("appearance.theme"))
101
115
  raise Vanken::ConfigError, "invalid font size" unless get("appearance.font_size").is_a?(Integer) && get("appearance.font_size").between?(8, 32)
116
+ raise Vanken::ConfigError, "invalid language" unless %w[ja en].include?(get("appearance.language"))
102
117
  raise Vanken::ConfigError, "invalid time format" unless %w[relative absolute delta delta_displayed epoch].include?(get("packet_list.time_format"))
103
118
  raise Vanken::ConfigError, "invalid time precision" unless %w[milli micro nano].include?(get("packet_list.time_precision"))
104
119
  raise Vanken::ConfigError, "invalid history" unless history.all? { |item| item.is_a?(String) } && recent_files.all? { |item| item.is_a?(String) } && bookmarks.all? { |name, expression| name.is_a?(String) && expression.is_a?(String) }
@@ -111,6 +126,17 @@ module Vanken
111
126
  columns = get("layout.columns")
112
127
  raise Vanken::ConfigError, "invalid columns" unless columns.all? { |column| column.is_a?(Hash) && %w[no time source destination protocol length info].include?(column["key"]) && column["width"].is_a?(Numeric) && column["width"].finite? && column["width"].between?(40, 4096) && [true, false].include?(column["visible"]) } && columns.map { |column| column["key"] }.uniq.size == columns.size
113
128
  raise Vanken::ConfigError, "invalid capture settings" unless %w[auto direct sudo pkexec].include?(get("capture.launcher")) && %w[auto ring socket bpf].include?(get("capture.backend")) && %w[in out inout].include?(get("capture.direction"))
129
+ %w[capture.stop_count capture.stop_bytes capture.ring_max_bytes].each do |path|
130
+ raise ConfigError, "invalid #{path}" unless get(path).is_a?(Integer) && get(path).between?(0, (1 << 63) - 1)
131
+ end
132
+ %w[capture.stop_duration capture.ring_interval].each do |path|
133
+ value = get(path)
134
+ raise ConfigError, "invalid #{path}" unless value.is_a?(Numeric) && value.finite? && value >= 0
135
+ end
136
+ raise ConfigError, "invalid ring file count" unless get("capture.ring_file_count").between?(1, 10_000)
137
+ raise ConfigError, "invalid ring path" if get("capture.ring_path").include?("\0")
138
+ raise ConfigError, "invalid resolver timeout" unless get("name_resolution.timeout").finite? && get("name_resolution.timeout").between?(0.01, 10)
139
+ raise ConfigError, "invalid resolver cache size" unless get("name_resolution.cache_size").between?(1, 10_000)
114
140
  rescue NoMethodError, KeyError
115
141
  raise Vanken::ConfigError, "invalid preferences shape"
116
142
  end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "preferences"
4
+ require_relative "yaml_file"
5
+
6
+ module Vanken
7
+ module Config
8
+ class Profiles
9
+ FILES = %w[preferences.yml coloring_rules.yml columns.yml filters.yml decode_as.yml plugins.yml].freeze
10
+ attr_reader :directory, :active
11
+
12
+ def initialize(directory: Paths.config)
13
+ @directory = File.expand_path(directory)
14
+ @active = YamlFile.read(File.join(@directory, "profiles.yml"), default: {"active" => "default"}).fetch("active", "default")
15
+ profile_directory(@active)
16
+ @active = "default" unless names.include?(@active)
17
+ end
18
+
19
+ def names
20
+ ["default", *Dir.glob(File.join(@directory, "profiles", "*")).select { |path| File.directory?(path) && !File.symlink?(path) }.map { |path| File.basename(path) }.sort]
21
+ end
22
+
23
+ def preferences
24
+ shared = @directory == File.expand_path(Paths.config) ? Paths.state : File.join(@directory, "state")
25
+ Preferences.new(directory: profile_directory(@active), state_directory: shared)
26
+ end
27
+
28
+ def create(name, copy_from: nil)
29
+ target = profile_directory(name)
30
+ raise ConfigError, "profile already exists" if names.include?(name) || File.exist?(target) || File.symlink?(target)
31
+ raise ConfigError, "unknown profile" if copy_from && !names.include?(copy_from)
32
+ FileUtils.mkdir_p(File.dirname(target), mode: 0o700)
33
+ Dir.mkdir(target, 0o700)
34
+ if copy_from
35
+ source = profile_directory(copy_from)
36
+ FILES.each do |file|
37
+ path = File.join(source, file)
38
+ next unless File.file?(path)
39
+ raise ConfigError, "profile file must not be a symlink" if File.symlink?(path)
40
+ File.write(File.join(target, file), File.binread(path), mode: "wb", perm: 0o600)
41
+ end
42
+ else
43
+ Preferences.new(directory: target).save
44
+ end
45
+ name
46
+ end
47
+
48
+ def switch(name)
49
+ profile_directory(name)
50
+ raise ConfigError, "unknown profile" unless names.include?(name)
51
+ @active = name
52
+ YamlFile.write(File.join(@directory, "profiles.yml"), "active" => name)
53
+ preferences
54
+ end
55
+
56
+ def delete(name)
57
+ path = profile_directory(name)
58
+ raise ConfigError, "cannot delete the active or default profile" if name == @active || name == "default"
59
+ raise ConfigError, "unknown profile" unless names.include?(name)
60
+ FileUtils.remove_entry_secure(path)
61
+ end
62
+
63
+ private
64
+
65
+ def profile_directory(name)
66
+ raise ConfigError, "invalid profile name" unless name.is_a?(String) && name.match?(/\A[\p{L}\p{N}_ -][\p{L}\p{N}_. -]{0,63}\z/) && !%w[. ..].include?(name)
67
+ path = name == "default" ? @directory : File.join(@directory, "profiles", name)
68
+ raise ConfigError, "profiles directory must not be a symlink" if name != "default" && File.symlink?(File.join(@directory, "profiles"))
69
+ raise ConfigError, "profile must not be a symlink" if File.symlink?(path)
70
+ path
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tmpdir"
4
+ require "json"
5
+
6
+ module Vanken
7
+ module Config
8
+ module Sessions
9
+ def self.candidates(parent: Dir.tmpdir)
10
+ Dir.glob(File.join(parent, "vanken-*" )).select { |directory| orphan?(directory) }.sort
11
+ end
12
+
13
+ def self.recover(directory, parent: Dir.tmpdir, **options)
14
+ with_session(directory, parent: parent) { App::Document.recover(directory, **options) }
15
+ end
16
+
17
+ def self.discard(directory, parent: Dir.tmpdir)
18
+ store = with_session(directory, parent: parent) { Core::FrameStore.recover(directory) }
19
+ store.close
20
+ end
21
+
22
+ def self.orphan?(directory)
23
+ return false unless File.directory?(directory) && !File.symlink?(directory)
24
+ stat = File.stat(directory)
25
+ return false unless stat.uid == Process.uid && (stat.mode & 0o077).zero?
26
+ Dir.children(directory).each do |name|
27
+ stat = File.lstat(File.join(directory, name))
28
+ return false unless stat.file? && stat.uid == Process.uid && stat.nlink == 1
29
+ end
30
+ path = File.join(directory, "session.json")
31
+ return false unless File.file?(path) && File.size(path) <= 65_536
32
+ return false unless %w[frames.bin frames.idx].all? { |name| File.file?(File.join(directory, name)) }
33
+ session = JSON.parse(File.read(path))
34
+ pid = session["pid"]
35
+ return false unless session["schema_version"] == 1 && pid.is_a?(Integer) && pid.positive?
36
+ begin
37
+ Process.kill(0, pid)
38
+ false
39
+ rescue Errno::ESRCH
40
+ true
41
+ rescue Errno::EPERM
42
+ false
43
+ end
44
+ rescue SystemCallError, JSON::ParserError, TypeError, NoMethodError
45
+ false
46
+ end
47
+
48
+ def self.with_session(directory, parent:)
49
+ path = File.expand_path(directory)
50
+ raise Vanken::FileError, "session is active or unsafe" unless candidates(parent: parent).include?(path)
51
+ File.open(File.join(path, "session.json"), "r+b") do |file|
52
+ raise Vanken::FileError, "session is being recovered" unless file.flock(File::LOCK_EX | File::LOCK_NB)
53
+ raise Vanken::FileError, "session is active or unsafe" unless orphan?(path)
54
+ original = file.read
55
+ begin
56
+ yield
57
+ rescue StandardError
58
+ file.rewind
59
+ file.truncate(0)
60
+ file.write(original)
61
+ file.flush
62
+ raise
63
+ end
64
+ end
65
+ end
66
+ private_class_method :orphan?, :with_session
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+ require "tempfile"
5
+ require "fileutils"
6
+
7
+ module Vanken
8
+ module Config
9
+ module YamlFile
10
+ def self.read(path, default: {})
11
+ return default unless File.file?(path)
12
+ raise ConfigError, "configuration exceeds 1 MiB" if File.size(path) > 1 << 20
13
+ value = YAML.safe_load(File.read(path), permitted_classes: [], aliases: false)
14
+ raise ConfigError, "unsupported configuration schema: #{path}" unless value.is_a?(Hash) && value["schema_version"] == 1
15
+ value
16
+ rescue Psych::Exception => error
17
+ raise ConfigError, error.message
18
+ end
19
+
20
+ def self.write(path, value)
21
+ directory = File.dirname(path)
22
+ FileUtils.mkdir_p(directory, mode: 0o700)
23
+ temporary = Tempfile.create(["vanken-", ".yml"], directory)
24
+ temporary.chmod(0o600)
25
+ temporary.write(YAML.dump(value.merge("schema_version" => 1)))
26
+ temporary.flush
27
+ temporary.fsync
28
+ temporary.close
29
+ File.rename(temporary.path, path)
30
+ ensure
31
+ temporary&.close unless temporary&.closed?
32
+ File.unlink(temporary.path) if temporary && File.exist?(temporary.path)
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ require "yaml"
5
+ require "tempfile"
6
+ require "fileutils"
7
+ require_relative "display_filter/compiler"
8
+
9
+ module Vanken
10
+ module Core
11
+ module Coloring
12
+ Rule = Data.define(
13
+ :name, #: String
14
+ :filter, #: String
15
+ :light, #: Hash[String, String]
16
+ :dark, #: Hash[String, String]
17
+ :enabled, #: bool
18
+ :program #: DisplayFilter::Program
19
+ )
20
+
21
+ class RuleSet
22
+ DEFAULT_PATH = File.expand_path("../../../data/coloring_rules.yml", __dir__)
23
+ attr_reader :rules #: Array[Rule]
24
+
25
+ # @rbs (Hash[String, untyped] payload, ?catalog: untyped) -> void
26
+ def initialize(payload, catalog: nil)
27
+ raise Vanken::ConfigError, "unsupported coloring rules schema" unless payload.is_a?(Hash) && payload["schema_version"] == 1 && payload["rules"].is_a?(Array) && payload["rules"].size <= 256
28
+ @rules = payload.fetch("rules").map do |value|
29
+ raise Vanken::ConfigError, "invalid coloring rule" unless value.is_a?(Hash) && value["name"].is_a?(String) && value["filter"].is_a?(String) && !value["filter"].empty? && [true, false].include?(value["enabled"])
30
+ palettes = %w[light dark].map do |theme|
31
+ colors = value[theme]
32
+ raise Vanken::ConfigError, "invalid rule colors" unless colors.is_a?(Hash) && %w[fg bg].all? { |key| colors[key].is_a?(String) && /\A#[\da-fA-F]{6}\z/.match?(colors[key]) }
33
+ colors.slice("fg", "bg").transform_values { |color| color.dup.freeze }.freeze
34
+ end
35
+ Rule.new(value["name"].dup.freeze, value["filter"].dup.freeze, palettes[0], palettes[1], value["enabled"], DisplayFilter.compile(value["filter"], catalog: catalog))
36
+ end.freeze
37
+ rescue DisplayFilter::SyntaxError => error
38
+ raise Vanken::ConfigError, "invalid coloring filter: #{error.message}"
39
+ end
40
+
41
+ # @rbs (_FilterView view, ?theme: Symbol) -> Hash[Symbol, String]?
42
+ def match(view, theme: :light)
43
+ rule = @rules.find { |item| item.enabled && item.program.match?(view) }
44
+ return nil unless rule
45
+ palette = theme == :light ? rule.light : rule.dark
46
+ colors = {foreground: palette.fetch("fg")}
47
+ colors[:background] = palette.fetch("bg") unless theme == :high_contrast
48
+ colors
49
+ end
50
+
51
+ # @rbs () -> Hash[String, untyped]
52
+ def to_h
53
+ {"schema_version" => 1, "rules" => @rules.map do |rule|
54
+ {"name" => rule.name, "filter" => rule.filter, "light" => rule.light.dup, "dark" => rule.dark.dup, "enabled" => rule.enabled}
55
+ end}
56
+ end
57
+
58
+ # @rbs (String path) -> void
59
+ def save(path)
60
+ directory = File.dirname(File.expand_path(path))
61
+ FileUtils.mkdir_p(directory, mode: 0o700)
62
+ temporary = Tempfile.create(["coloring-", ".tmp"], directory)
63
+ temporary.write(YAML.dump(to_h))
64
+ temporary.flush
65
+ temporary.fsync
66
+ temporary.close
67
+ File.rename(temporary.path, path)
68
+ ensure
69
+ temporary&.close unless temporary&.closed?
70
+ File.unlink(temporary.path) if temporary && File.exist?(temporary.path)
71
+ end
72
+
73
+ # @rbs (String path, ?catalog: untyped) -> RuleSet
74
+ def self.load(path, catalog: nil)
75
+ raise Vanken::ConfigError, "coloring rules file exceeds 1 MiB" if File.size(path) > 1 << 20
76
+ new(YAML.safe_load(File.read(path), permitted_classes: [], aliases: false), catalog: catalog)
77
+ rescue Psych::Exception, SystemCallError => error
78
+ raise Vanken::ConfigError, error.message
79
+ end
80
+
81
+ # @rbs (?catalog: untyped) -> RuleSet
82
+ def self.defaults(catalog: nil) = load(DEFAULT_PATH, catalog: catalog)
83
+ end
84
+ end
85
+ end
86
+ end
@@ -114,8 +114,8 @@ module Vanken
114
114
  [@data, @records, @reader, @index_reader].compact.each(&:close)
115
115
  @closed = true
116
116
  end
117
+ FileUtils.remove_entry_secure(@directory) if remove && File.directory?(@directory)
117
118
  end
118
- FileUtils.remove_entry_secure(@directory) if remove && File.directory?(@directory)
119
119
  end
120
120
 
121
121
  private
@@ -130,7 +130,7 @@ module Vanken
130
130
 
131
131
  def validate_directory
132
132
  raise Vanken::FileError, "unsafe session directory" unless File.directory?(@directory) && !File.symlink?(@directory) && File.stat(@directory).uid == Process.uid
133
- %w[frames.idx frames.bin interfaces.json session.json].each do |name|
133
+ Dir.children(@directory).each do |name|
134
134
  path = File.join(@directory, name)
135
135
  next unless File.exist?(path) || File.symlink?(path)
136
136
  stat = File.lstat(path)
@@ -116,12 +116,14 @@ module Vanken
116
116
  SIZE = 32
117
117
  attr_reader :streams #: Hash[Integer, Array[Integer]]
118
118
  attr_reader :experts #: Hash[Integer, Array[Hash[Symbol, untyped]]]
119
+ attr_reader :expert_count, :expert_max #: Integer
119
120
  # @rbs! @extra: Hash[Integer, Hash[String, untyped]]
120
121
  # @rbs! @io: File?
121
122
  # @rbs (String directory, ?persist: bool) -> void
122
123
  def initialize(directory, persist: true)
123
124
  @data = +"".b
124
125
  @extra, @streams, @experts = {}, Hash.new { |hash, key| hash[key] = [] }, {}
126
+ @expert_count = @expert_max = 0
125
127
  @io = File.open(File.join(directory, "annotations.bin"), "wb", 0o600) if persist
126
128
  end
127
129
  # @rbs (Integer number, annotation_values annotation) -> void
@@ -132,6 +134,8 @@ module Vanken
132
134
  @io&.write(record)
133
135
  @streams[annotation[:tcp_stream]] << number if annotation[:tcp_stream] >= 0
134
136
  @experts[number] = annotation[:expert_items] unless annotation[:expert_items].empty?
137
+ @expert_count += annotation[:expert_items].size
138
+ @expert_max = [@expert_max, annotation[:expert_max]].max
135
139
  @extra[number] = annotation[:extra] unless annotation[:extra].empty?
136
140
  end
137
141
  # @rbs () -> Hash[Symbol, untyped]
@@ -139,6 +143,7 @@ module Vanken
139
143
  batch = {data: @data, extras: @extra, streams: @streams.transform_values { |numbers| numbers }, experts: @experts}
140
144
  @data = +"".b
141
145
  @extra, @streams, @experts = {}, Hash.new { |hash, key| hash[key] = [] }, {}
146
+ @expert_count = @expert_max = 0
142
147
  batch
143
148
  end
144
149
  # @rbs (Hash[Symbol, untyped] batch) -> void
@@ -148,6 +153,13 @@ module Vanken
148
153
  @data << batch[:data]
149
154
  @extra.merge!(batch[:extras])
150
155
  @experts.merge!(batch[:experts])
156
+ batch[:experts].each_value do |items|
157
+ @expert_count += items.size
158
+ items.each do |item|
159
+ rank = {"note" => 1, "warn" => 2, "warning" => 2, "error" => 3}.fetch(item[:severity].to_s, 0)
160
+ @expert_max = [@expert_max, rank].max
161
+ end
162
+ end
151
163
  batch[:streams].each { |stream, numbers| @streams[stream].concat(numbers) }
152
164
  end
153
165
  # @rbs (Integer number) -> annotation_values
@@ -12,7 +12,7 @@ module Vanken
12
12
  frame = node("frame", "Frame #{raw.number}: #{raw.original_length} bytes on wire, #{raw.caplen} captured", offset: 0, length: raw.caplen)
13
13
  nodes = packet.layers.each_with_index.map do |layer, index|
14
14
  prefix = packet.layers.count { |item| item.protocol == layer.protocol } > 1 ? "#{layer.protocol}[#{index}]" : layer.protocol.to_s
15
- klass = Redhound::Registry.default.protocols[layer.protocol]
15
+ klass = packet.registry.protocols[layer.protocol]
16
16
  source = packet.layer_source(layer)
17
17
  children = layer.fields.map do |field|
18
18
  value = field.value