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,233 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../config/profiles"
4
+ require_relative "../config/analysis_settings"
5
+
6
+ module Vanken
7
+ module UI
8
+ module SettingsOperations
9
+ SETTING_LABELS = {
10
+ "appearance.theme" => "テーマ", "appearance.language" => "言語", "appearance.font_size" => "文字サイズ",
11
+ "packet_list.time_format" => "時刻表示", "packet_list.time_precision" => "時刻の精度", "packet_list.autoscroll" => "自動スクロール",
12
+ "packet_list.row_cache_rows" => "行キャッシュ容量", "packet_list.coloring" => "色付け",
13
+ "capture.snaplen" => "Snaplen", "capture.promiscuous" => "プロミスキャスモード", "capture.buffer_size" => "取得バッファ (bytes)",
14
+ "capture.backend" => "キャプチャ方式", "capture.launcher" => "ヘルパー起動方式", "capture.direction" => "取得方向",
15
+ "capture.stop_count" => "自動停止: パケット数 (0: 無効)", "capture.stop_duration" => "自動停止: 秒 (0: 無効)", "capture.stop_bytes" => "自動停止: bytes (0: 無効)",
16
+ "capture.ring_path" => "リング保存先 (空欄: 無効)", "capture.ring_max_bytes" => "リング: ファイルごとのbytes", "capture.ring_interval" => "リング: 秒", "capture.ring_file_count" => "リング: 保持ファイル数",
17
+ "analysis.verify_checksums" => "チェックサム検証", "analysis.max_state_mib" => "解析メモリ上限(MiB)", "analysis.max_flows" => "フロー数上限", "analysis.workers" => "ワーカー数",
18
+ "name_resolution.enabled" => "名前解決", "name_resolution.timeout" => "解決タイムアウト(秒)", "name_resolution.cache_size" => "名前キャッシュ容量"
19
+ }.freeze
20
+ def analysis_settings = Config::AnalysisSettings.new(directory: @preferences.directory)
21
+
22
+ def preferences_dialog
23
+ groups = %w[appearance packet_list capture analysis name_resolution].map do |group|
24
+ defaults = Config::Preferences::DEFAULTS.fetch(group)
25
+ schema = defaults.map do |key, value|
26
+ path = "#{group}.#{key}"
27
+ options = {"appearance.theme" => %w[system dark light high_contrast], "appearance.language" => %w[ja en],
28
+ "packet_list.time_format" => %w[relative absolute delta delta_displayed epoch], "packet_list.time_precision" => %w[milli micro nano],
29
+ "capture.backend" => %w[auto ring socket bpf], "capture.launcher" => %w[auto direct sudo pkexec], "capture.direction" => %w[in out inout]}[path]
30
+ captions = {"system" => "システム設定", "dark" => "ダーク", "light" => "ライト", "high_contrast" => "ハイコントラスト",
31
+ "ja" => "日本語", "en" => "English", "relative" => "相対時刻", "absolute" => "絶対時刻", "delta" => "前パケットとの差", "delta_displayed" => "前表示パケットとの差", "epoch" => "エポック時刻",
32
+ "milli" => "ミリ秒", "micro" => "マイクロ秒", "nano" => "ナノ秒", "in" => "受信", "out" => "送信", "inout" => "双方向"}
33
+ options = options.map { |item| [t(captions.fetch(item, item)), item] } if options
34
+ type = options ? :select : [true, false].include?(value) ? :boolean : value.is_a?(Numeric) ? :number : :text
35
+ {key: path, label: setting_label(path), type: type, options: options || []}
36
+ end
37
+ values = defaults.keys.to_h { |key| [:"#{group}.#{key}", @preferences.get("#{group}.#{key}")] }
38
+ grid = Zaniah::UI::PropertyGrid.new(schema, values, height: 240, label: setting_label(group)).test_id("vk.preferences.#{group}")
39
+ .on_change { |key, value, *_| apply_preference(key.to_s, value) }
40
+ [setting_label(group), grid]
41
+ end
42
+ content = Zaniah::Div.new.flex_col.gap(8)
43
+ .child(Zaniah::UI::Button.new(t("プロファイルの管理")).on_click { profiles_dialog })
44
+ .child(Zaniah::UI::Tabs.new(groups))
45
+ show_dialog(:preferences, t("設定"), content, reopen: -> { preferences_dialog })
46
+ end
47
+
48
+ def apply_preference(path, value)
49
+ old = @preferences.get(path)
50
+ value = Integer(value) if old.is_a?(Integer)
51
+ value = Float(value) if old.is_a?(Float)
52
+ @preferences.set(path, value)
53
+ if path.start_with?("appearance.")
54
+ change_theme(@preferences.get("appearance.theme"), persist: false)
55
+ rebuild_view if path == "appearance.language"
56
+ elsif path.start_with?("packet_list.", "name_resolution.")
57
+ reset_resolver if path.start_with?("name_resolution.")
58
+ if path == "packet_list.autoscroll"
59
+ @autoscroll = value
60
+ @table.follow_tail = value if @table
61
+ end
62
+ @packet_source.reset
63
+ elsif path.start_with?("analysis.")
64
+ @pending_analysis_settings = true
65
+ apply_analysis_settings
66
+ end
67
+ @window.request_frame
68
+ rescue StandardError => error
69
+ show_error(error)
70
+ end
71
+
72
+ def profiles_dialog
73
+ content = Zaniah::Div.new.flex_col.gap(8)
74
+ @profiles.names.each do |name|
75
+ row = Zaniah::Div.new.flex_row.gap(6)
76
+ .child(Zaniah::UI::Button.new(name, variant: name == @profiles.active ? :secondary : :ghost).on_click { switch_profile(name) }.flex_1)
77
+ .child(Zaniah::UI::Button.new(t("複製")).on_click { path_dialog(t("新しいプロファイル名")) { |value| @profiles.create(value, copy_from: name); profiles_dialog } })
78
+ row.child(Zaniah::UI::Button.new(t("削除")).disabled(name == @profiles.active || name == "default").on_click { @profiles.delete(name); profiles_dialog })
79
+ content.child(row)
80
+ end
81
+ content.child(Zaniah::UI::Button.new(t("新規作成")).on_click { path_dialog(t("新しいプロファイル名")) { |value| @profiles.create(value); profiles_dialog } })
82
+ show_dialog(:profiles, t("プロファイル"), Zaniah::ScrollView.new.h(240).child(content), reopen: -> { profiles_dialog })
83
+ end
84
+
85
+ def switch_profile(name)
86
+ raise Vanken::Error, "stop capture before switching profiles" if @capture.running?
87
+ raise Vanken::Error, "finish loading before switching profiles" if document&.loading? && !@reanalysis_in_flight
88
+ close_analysis if respond_to?(:close_analysis)
89
+ @preferences = @profiles.switch(name)
90
+ @capture.preferences = @preferences
91
+ reset_resolver
92
+ reload_columns
93
+ reload_coloring_rules
94
+ change_theme(@preferences.get("appearance.theme"), persist: false)
95
+ rebuild_view
96
+ dismiss_dialog
97
+ reconfigure_analysis if document
98
+ rescue StandardError => error
99
+ show_error(error)
100
+ end
101
+
102
+ def decode_as
103
+ field = Zaniah::UI::TextField.new(analysis_settings.decode_as.join("; "), label: t("例: udp.port==8443,dns")).test_id("vk.decode_as.rules")
104
+ content = Zaniah::Div.new.flex_col.gap(12).child(field)
105
+ .child(Zaniah::UI::Button.new(t("適用")).on_click do
106
+ rules = field.value.split(";").map(&:strip).reject(&:empty?)
107
+ Gateway::Dissector.new(decode_as: rules, plugins: analysis_settings.plugins)
108
+ analysis_settings.decode_as = rules
109
+ dismiss_dialog
110
+ reconfigure_analysis
111
+ rescue StandardError => error
112
+ show_error(error)
113
+ end)
114
+ show_dialog(:decode_as, t("別プロトコルとして解析"), content, reopen: -> { decode_as })
115
+ end
116
+
117
+ def plugins_dialog
118
+ content = Zaniah::Div.new.flex_col.gap(8)
119
+ analysis_settings.plugin_entries.each do |entry|
120
+ path = entry.fetch("path")
121
+ content.child(Zaniah::Div.new.flex_row.gap(8).child(Zaniah::UI::Label.new(path, wrap: :word).flex_1)
122
+ .child(Zaniah::UI::Button.new(t("削除")).on_click { analysis_settings.remove_plugin(path); plugins_dialog }))
123
+ end
124
+ content.child(Zaniah::UI::Button.new(t("追加")).on_click { path_dialog(t("Ruby ディセクタのパス")) { |path| analysis_settings.register_plugin(path); reload_plugins } })
125
+ content.child(Zaniah::UI::Button.new(t("再読み込み")).on_click { reload_plugins })
126
+ show_dialog(:plugins, t("ディセクタプラグイン"), Zaniah::ScrollView.new.h(240).child(content), reopen: -> { plugins_dialog })
127
+ end
128
+
129
+ def reload_plugins
130
+ paths = analysis_settings.untrusted_plugins
131
+ return reconfigure_analysis if paths.empty?
132
+ content = Zaniah::Div.new.flex_col.gap(12).child(Zaniah::UI::Label.new(t("Ruby プラグインはユーザー権限で任意のコードを実行します。信頼するファイルだけを読み込んでください。\n%{paths}", paths: paths.join("\n")), wrap: :word))
133
+ .child(Zaniah::UI::Button.new(t("信頼して読み込む")).test_id("vk.plugins.trust").on_click { analysis_settings.trust_plugins(paths); dismiss_dialog; reconfigure_analysis })
134
+ .child(Zaniah::UI::Button.new(t("キャンセル"), variant: :secondary).on_click { dismiss_dialog })
135
+ show_dialog(:plugin_trust, t("プラグインの確認"), content, reopen: -> { reload_plugins })
136
+ end
137
+
138
+ def reconfigure_analysis
139
+ doc = document
140
+ return unless doc
141
+ raise Vanken::Error, "finish loading or stop capture before changing analysis" if @capture.running? || (doc.loading? && !@reanalysis_in_flight)
142
+ @pending_reanalysis = {document: doc, options: {decode_as: analysis_settings.decode_as, plugins: analysis_settings.plugins, preferences: @preferences}, number: selected_number}
143
+ begin_reanalysis
144
+ end
145
+
146
+ def begin_reanalysis
147
+ request = @pending_reanalysis
148
+ return unless request && !document&.loading? && (!@reanalysis_task || @reanalysis_task.done?)
149
+ @pending_reanalysis = nil
150
+ doc = request[:document]
151
+ return unless document.equal?(doc) && !@closing
152
+ close_analysis if respond_to?(:close_analysis)
153
+ @selection_generation += 1
154
+ selection_generation = @selection_generation
155
+ @reanalysis_selection = nil
156
+ @reanalysis_in_flight = true
157
+ @active_reanalysis = request
158
+ @packet_source.reset
159
+ @reanalysis_task = @app.executor.background do
160
+ doc.reanalyze(**request[:options])
161
+ doc.cancel_reanalysis unless @active_reanalysis.equal?(request)
162
+ doc.wait
163
+ raise doc.error if doc.error
164
+ @app.executor.post do
165
+ next if @closing || !document.equal?(doc)
166
+ @packet_source.reset
167
+ @reanalysis_selection = [doc, request[:number], selection_generation]
168
+ changed
169
+ end
170
+ rescue StandardError => error
171
+ @app.executor.post do
172
+ next if @closing || !document.equal?(doc)
173
+ @reanalysis_in_flight = false
174
+ show_error(error)
175
+ begin_reanalysis
176
+ end
177
+ end
178
+ @reanalysis_task.on_complete { @app.executor.post { changed if !@closing && document.equal?(doc) } }
179
+ end
180
+
181
+ def wait_reanalysis
182
+ task, @reanalysis_task = @reanalysis_task, nil
183
+ @active_reanalysis = nil
184
+ document&.cancel_reanalysis if @reanalysis_in_flight && task && !task.done?
185
+ task&.await
186
+ end
187
+
188
+ def apply_analysis_settings
189
+ return unless @pending_analysis_settings
190
+ return if @capture.running? || document&.loading?
191
+ @pending_analysis_settings = false
192
+ document&.cancel_scan
193
+ @pool_mutex.synchronize do
194
+ @pool&.shutdown
195
+ @pool = nil
196
+ end
197
+ reconfigure_analysis if document
198
+ end
199
+
200
+ def command_palette
201
+ dismiss_dialog
202
+ @dialog_kind = :command_palette
203
+ @dialog_reopen = -> { command_palette }
204
+ @dialog = Zaniah::UI::CommandPalette.from(@app.actions, open: true, placeholder: t("コマンドを検索"), title: t("コマンドパレット"), close_label: t("閉じる")).test_id("vk.command_palette")
205
+ .on_close { dismiss_dialog }
206
+ @window.request_frame
207
+ end
208
+
209
+ def rebuild_view
210
+ expanded = @tree&.expanded&.dup || []
211
+ text = @filter_field.value
212
+ @filter_field = Zaniah::UI::TextField.new(text, placeholder: t("表示フィルタ"), clearable: false).test_id("vk.filter.input")
213
+ @filter_field.completion(CompletionProvider.new(self)).on_change { |value, _| validate_filter(value) }
214
+ Actions.install(self)
215
+ @main_view = MainView.new(self).test_id("vk.main")
216
+ @tree.replace(detail_nodes)
217
+ expanded.each { |id| @tree.expand(id) }
218
+ @hex.bytes = selected_bytes
219
+ @table.selection.add(selected_number) if selected_number
220
+ @packet_source.reset
221
+ reopen_dialog
222
+ @window.request_frame
223
+ end
224
+
225
+ private
226
+
227
+ def setting_label(path)
228
+ label = SETTING_LABELS[path] || {"appearance" => "表示", "packet_list" => "パケット一覧", "capture" => "キャプチャ", "analysis" => "解析", "name_resolution" => "名前解決"}.fetch(path, path)
229
+ t(label)
230
+ end
231
+ end
232
+ end
233
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Vanken
4
- VERSION = "0.1.0"
4
+ VERSION = "0.3.0"
5
5
  end
data/packaging/README.md CHANGED
@@ -1,5 +1,45 @@
1
1
  # Live capture permissions
2
2
 
3
+ `vanken-setup-permissions` installs the files shipped in `packaging/linux/` or `packaging/macos/`. Review the macOS plan first:
4
+
5
+ ```sh
6
+ vanken-setup-permissions --platform darwin --dry-run
7
+ ```
8
+
9
+ On Linux, provide the canonical absolute paths of a root-owned Ruby 3.3 or newer runtime and its isolated gem home:
10
+
11
+ ```sh
12
+ vanken-setup-permissions --platform linux --dry-run \
13
+ --ruby /opt/vanken-ruby/bin/ruby --gem-home /opt/vanken-ruby/gems
14
+ ```
15
+
16
+ The following administrator installation assumes Ruby is already installed under `/opt/vanken-ruby`, with root-owned runtime files that are not writable by group or other users. Install Vanken into the isolated root-owned gem home, then run its packaged setup command with that same interpreter:
17
+
18
+ ```sh
19
+ sudo /usr/bin/env -i PATH=/usr/bin:/bin GEM_HOME=/opt/vanken-ruby/gems GEM_PATH=/opt/vanken-ruby/gems \
20
+ /opt/vanken-ruby/bin/ruby /opt/vanken-ruby/bin/gem install vanken --version 0.3.0 --no-document
21
+ sudo /usr/bin/env -i PATH=/usr/bin:/bin GEM_HOME=/opt/vanken-ruby/gems GEM_PATH=/opt/vanken-ruby/gems \
22
+ /opt/vanken-ruby/bin/ruby /opt/vanken-ruby/gems/bin/vanken-setup-permissions \
23
+ --ruby /opt/vanken-ruby/bin/ruby --gem-home /opt/vanken-ruby/gems --policy polkit
24
+ ```
25
+
26
+ The installer checks the interpreter, its library paths, runtime tree, gem home, and source tree for root ownership and writable paths before copying anything. Root-owned runtime library aliases are allowed only when every link, target, and parent path passes the same checks. The wrapper clears all inherited environment variables, fixes the interpreter and gem paths, and runs only `/usr/local/libexec/vanken/helper`. The polkit policy authorizes that exact wrapper path. Launch Vanken as your ordinary user; polkit provides the administrator authentication dialog when capture requires elevation.
27
+
28
+ For headless Linux or sudo instead of polkit, create a dedicated capture group and add only users who should inspect network traffic. Log out and log in after changing group membership, then use `--policy sudoers --group vanken` in the setup command. The installer requires `visudo` and validates the rule before installing `/etc/sudoers.d/vanken`. It never grants capabilities to a shared Ruby interpreter.
29
+
30
+ On macOS, create the intended group, install the BPF daemon, and activate it:
31
+
32
+ ```sh
33
+ sudo dseditgroup -o create vanken
34
+ sudo dseditgroup -o edit -a "$USER" -t user vanken
35
+ sudo "$(command -v vanken-setup-permissions)" --group vanken
36
+ sudo launchctl bootstrap system /Library/LaunchDaemons/org.vanken.chmod-bpf.plist
37
+ ```
38
+
39
+ Log out and log in after joining the group. The daemon grants the group mode `0660` on existing BPF devices at startup and every ten seconds, including devices added later. It runs the fixed root-owned shell script; Vanken and the capture helper run as the normal user. Verify access with `vanken-capture --check --interface lo0`. If Wireshark's ChmodBPF already manages these devices, use that configuration instead of installing a second daemon.
40
+
41
+ Use `--root /absolute/staging-directory` to prepare files in an existing directory without activating system permissions. A staged tree is for review and packaging; it does not validate or grant trust to the runtime paths. Neither `--dry-run` nor staging loads the macOS daemon. To remove the installed macOS configuration, unload `org.vanken.chmod-bpf` with `launchctl bootout`, remove its plist and `chmod-bpf` script, and restore the BPF device ownership/permissions prescribed by your administrator.
42
+
3
43
  The application runs as your normal user. The capture helper opens the capture device, drops supplementary groups and both real and effective group/user IDs, and writes pcapng to stdout. The interpreter, helper, libraries, gems, and every parent directory of the privileged wrapper must be owned by root and must not be writable by group or other users.
4
44
 
5
45
  On macOS, grant the intended capture group access to `/dev/bpf*` using an administrator-managed startup configuration. Once `vanken-capture --check --interface lo0` reports `"direct":true`, Vanken can capture without sudo. Device permissions may need to be restored after a reboot.
@@ -30,4 +70,4 @@ Validate the rule with `visudo`. Members of this group can capture network traff
30
70
  sudo /usr/local/libexec/vanken/vanken-capture -i eth0 --drop-to "$(id -u):$(id -g)" > capture.pcapng
31
71
  ```
32
72
 
33
- For the isolated Linux integration check, run `script/capture-ci.sh` with Docker available. It creates a disposable privileged container, installs root-owned Ruby/gems and the fixed helper wrapper, creates the `vanken-test` namespace and a veth pair, then runs the tests as an unprivileged user. The tests verify UID dropping, packet capture, statistics, and clean shutdown. The nightly `Live capture` workflow runs this same command.
73
+ For the isolated Linux integration check, run `script/capture-ci.sh` with Docker available. It creates a disposable privileged container, runs `vanken-setup-permissions` against its root-owned Ruby/gems to install the fixed wrapper and sudoers policy, creates the `vanken-test` namespace and a veth pair, then runs the tests as an unprivileged user. The tests verify environment isolation, UID dropping, packet capture, statistics, and clean shutdown. The installer plan is saved as `permission-install.json`. The nightly `Live capture` workflow runs this same command.
@@ -0,0 +1,14 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE policyconfig PUBLIC "-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN" "http://www.freedesktop.org/standards/PolicyKit/1/policyconfig.dtd">
3
+ <policyconfig>
4
+ <action id="org.vanken.capture">
5
+ <description>Capture network packets with Vanken</description>
6
+ <message>Authentication is required to capture network packets.</message>
7
+ <defaults>
8
+ <allow_any>no</allow_any>
9
+ <allow_inactive>no</allow_inactive>
10
+ <allow_active>auth_admin_keep</allow_active>
11
+ </defaults>
12
+ <annotate key="org.freedesktop.policykit.exec.path">/usr/local/libexec/vanken/vanken-capture</annotate>
13
+ </action>
14
+ </policyconfig>
@@ -0,0 +1,6 @@
1
+ #!/bin/sh
2
+ cd / || exit 1
3
+ exec /usr/bin/env -i PATH=/usr/bin:/bin:/usr/sbin:/sbin \
4
+ GEM_HOME=VANKEN_GEM_HOME GEM_PATH=VANKEN_GEM_HOME \
5
+ VANKEN_RUBY -I /usr/local/libexec/vanken/lib \
6
+ /usr/local/libexec/vanken/helper "$@"
@@ -0,0 +1,9 @@
1
+ [Desktop Entry]
2
+ Type=Application
3
+ Name=Vanken
4
+ Comment=Capture and inspect network packets
5
+ Exec=vanken %f
6
+ Icon=network-workgroup
7
+ Terminal=false
8
+ Categories=Network;Development;
9
+ MimeType=application/vnd.tcpdump.pcap;application/x-pcapng;
@@ -0,0 +1,2 @@
1
+ # Members of this group can inspect network traffic. Validate with visudo -cf.
2
+ %VANKEN_GROUP ALL=(root) NOPASSWD: /usr/local/libexec/vanken/vanken-capture
@@ -0,0 +1,6 @@
1
+ #!/bin/sh
2
+ for device in /dev/bpf[0-9]*; do
3
+ [ -c "$device" ] || continue
4
+ /usr/bin/chgrp VANKEN_GROUP "$device" || exit 1
5
+ /bin/chmod 660 "$device" || exit 1
6
+ done
@@ -0,0 +1,8 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0"><dict>
4
+ <key>Label</key><string>org.vanken.chmod-bpf</string>
5
+ <key>ProgramArguments</key><array><string>/usr/local/libexec/vanken/chmod-bpf</string></array>
6
+ <key>RunAtLoad</key><true/>
7
+ <key>StartInterval</key><integer>10</integer>
8
+ </dict></plist>
@@ -3,6 +3,18 @@
3
3
  module Vanken
4
4
  module App
5
5
  class CaptureController
6
+ class RingReader
7
+ def initialize: (untyped reader, untyped writer, on_error: untyped) -> untyped
8
+
9
+ def next_frame: (timeout: untyped) -> untyped
10
+
11
+ def eof?: () -> untyped
12
+
13
+ def stop: () -> untyped
14
+
15
+ def close: () -> untyped
16
+ end
17
+
6
18
  class Error < Vanken::CaptureError
7
19
  attr_reader code: untyped
8
20
 
@@ -29,6 +41,8 @@ module Vanken
29
41
 
30
42
  def snapshot: () -> untyped
31
43
 
44
+ def preferences=: (untyped value) -> untyped
45
+
32
46
  def start: (untyped options) -> untyped
33
47
 
34
48
  def stop: () -> untyped
@@ -43,6 +57,8 @@ module Vanken
43
57
 
44
58
  def run: () -> untyped
45
59
 
60
+ def open_ring: () -> untyped
61
+
46
62
  def consume_control: (untyped io) -> untyped
47
63
 
48
64
  # Called under @mutex; the helper stop runs separately so UI callbacks never wait for it.
@@ -5,6 +5,10 @@ module Vanken
5
5
  class Document
6
6
  include DocumentJobs
7
7
 
8
+ include DocumentAnalysis
9
+
10
+ include Navigation
11
+
8
12
  attr_reader store: untyped
9
13
 
10
14
  attr_reader columns: untyped
@@ -31,11 +35,13 @@ module Vanken
31
35
 
32
36
  attr_reader analysis_options: untyped
33
37
 
38
+ attr_reader analysis_gateway_options: untyped
39
+
34
40
  attr_accessor frame_latency: untyped
35
41
 
36
42
  attr_accessor scanner: untyped
37
43
 
38
- def initialize: (?on_update: untyped, ?preferences: untyped, ?store: untyped, ?scanner: untyped, ?process_analysis: untyped) -> untyped
44
+ def initialize: (?on_update: untyped, ?preferences: untyped, ?store: untyped, ?scanner: untyped, ?process_analysis: untyped, ?decode_as: untyped, ?plugins: untyped) -> untyped
39
45
 
40
46
  def open: (untyped path) -> untyped
41
47
 
@@ -61,6 +67,12 @@ module Vanken
61
67
 
62
68
  def closing?: () -> untyped
63
69
 
70
+ def analysis_stopped?: () -> untyped
71
+
72
+ def capture_stats: () -> untyped
73
+
74
+ def expert_summary: () -> untyped
75
+
64
76
  def stream_frames: (untyped stream) -> untyped
65
77
 
66
78
  def packet: (untyped number) -> untyped
@@ -72,6 +84,14 @@ module Vanken
72
84
 
73
85
  def row: (untyped number) -> untyped
74
86
 
87
+ def custom_columns: () -> untyped
88
+
89
+ def custom_columns=: (untyped columns) -> untyped
90
+
91
+ def custom_row: (untyped number) -> untyped
92
+
93
+ def custom_sort_value: (untyped number, untyped field) -> untyped
94
+
75
95
  def details: (untyped number) -> untyped
76
96
 
77
97
  def publish: (untyped number, untyped packet) -> untyped
@@ -97,6 +117,8 @@ module Vanken
97
117
 
98
118
  def cancel: () -> untyped
99
119
 
120
+ def cancel_reanalysis: () -> untyped
121
+
100
122
  def wait: (?untyped timeout) -> untyped
101
123
 
102
124
  def close: () -> untyped
@@ -0,0 +1,21 @@
1
+ # Generated from lib/vanken/app/document_analysis.rb with RBS::Inline
2
+
3
+ module Vanken
4
+ module App
5
+ module DocumentAnalysis
6
+ def self.included: (untyped base) -> untyped
7
+
8
+ module ClassMethods
9
+ def recover: (untyped directory, **untyped options) -> untyped
10
+ end
11
+
12
+ def analyze_store: () -> untyped
13
+
14
+ def reanalyze: (?decode_as: untyped, ?plugins: untyped, ?preferences: untyped) -> untyped
15
+
16
+ private
17
+
18
+ def open_analysis_files: () -> untyped
19
+ end
20
+ end
21
+ end
@@ -21,6 +21,12 @@ module Vanken
21
21
 
22
22
  def stale_job?: (untyped generation) -> untyped
23
23
 
24
+ # Retain only post-filter arrivals, at eight bytes per packet, and only for
25
+ # displayed-delta filters. Rebuilds read this basis without replacing it.
26
+ def record_live_predecessors: (untyped first, untyped last, untyped matches, ?context: untyped) -> untyped
27
+
28
+ def reanalysis_snapshot: () -> untyped
29
+
24
30
  def filter_snapshot: (?displayed_delta: untyped) -> untyped
25
31
  end
26
32
  end
@@ -0,0 +1,30 @@
1
+ # Generated from lib/vanken/app/expert_info.rb with RBS::Inline
2
+
3
+ module Vanken
4
+ module App
5
+ class ExpertRow < Data
6
+ attr_reader severity(): untyped
7
+
8
+ attr_reader code(): untyped
9
+
10
+ attr_reader protocol(): untyped
11
+
12
+ attr_reader message(): untyped
13
+
14
+ attr_reader count(): untyped
15
+
16
+ attr_reader numbers(): untyped
17
+
18
+ def self.new: (untyped severity, untyped code, untyped protocol, untyped message, untyped count, untyped numbers) -> instance
19
+ | (severity: untyped, code: untyped, protocol: untyped, message: untyped, count: untyped, numbers: untyped) -> instance
20
+
21
+ def self.members: () -> [ :severity, :code, :protocol, :message, :count, :numbers ]
22
+
23
+ def members: () -> [ :severity, :code, :protocol, :message, :count, :numbers ]
24
+ end
25
+
26
+ module ExpertInfo
27
+ def self.rows: (untyped document, ?displayed_only: untyped) -> untyped
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,45 @@
1
+ # Generated from lib/vanken/app/io_graph.rb with RBS::Inline
2
+
3
+ module Vanken
4
+ module App
5
+ class IOPoint < Data
6
+ attr_reader interval(): untyped
7
+
8
+ attr_reader start_ns(): untyped
9
+
10
+ attr_reader packets(): untyped
11
+
12
+ attr_reader bytes(): untyped
13
+
14
+ def self.new: (untyped interval, untyped start_ns, untyped packets, untyped bytes) -> instance
15
+ | (interval: untyped, start_ns: untyped, packets: untyped, bytes: untyped) -> instance
16
+
17
+ def self.members: () -> [ :interval, :start_ns, :packets, :bytes ]
18
+
19
+ def members: () -> [ :interval, :start_ns, :packets, :bytes ]
20
+ end
21
+
22
+ class IOSeries < Data
23
+ attr_reader expression(): untyped
24
+
25
+ attr_reader interval_ns(): untyped
26
+
27
+ attr_reader points(): untyped
28
+
29
+ attr_reader omitted_intervals(): untyped
30
+
31
+ def self.new: (untyped expression, untyped interval_ns, untyped points, untyped omitted_intervals) -> instance
32
+ | (expression: untyped, interval_ns: untyped, points: untyped, omitted_intervals: untyped) -> instance
33
+
34
+ def self.members: () -> [ :expression, :interval_ns, :points, :omitted_intervals ]
35
+
36
+ def members: () -> [ :expression, :interval_ns, :points, :omitted_intervals ]
37
+ end
38
+
39
+ module IOGraph
40
+ INTERVALS: untyped
41
+
42
+ def self.build: (untyped document, ?interval: untyped, ?series: untyped, ?displayed_only: untyped, ?cancelled: untyped) -> untyped
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,31 @@
1
+ # Generated from lib/vanken/app/navigation.rb with RBS::Inline
2
+
3
+ module Vanken
4
+ module App
5
+ module Navigation
6
+ def displayed_index: (untyped number) -> untyped
7
+
8
+ def toggle_mark: (untyped number) -> untyped
9
+
10
+ def toggle_ignore: (untyped number) -> untyped
11
+
12
+ def toggle_time_reference: (untyped number) -> untyped
13
+
14
+ def mark_all_displayed: () -> untyped
15
+
16
+ def unmark_all: () -> untyped
17
+
18
+ def conversation_neighbor: (untyped number, untyped direction) -> untyped
19
+
20
+ def search: (untyped query, **untyped options) ?{ (?) -> untyped } -> untyped
21
+
22
+ def cancel_search: () -> untyped
23
+
24
+ private
25
+
26
+ def toggle_packet_state: (untyped values, untyped number, untyped field) -> untyped
27
+
28
+ def packet_states_changed: (untyped field) -> untyped
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,21 @@
1
+ # Generated from lib/vanken/app/search_job.rb with RBS::Inline
2
+
3
+ module Vanken
4
+ module App
5
+ class SearchJob
6
+ attr_reader program: untyped
7
+
8
+ def initialize: (untyped document, untyped query, ?mode: untyped, ?target: untyped, ?from: untyped, ?direction: untyped) -> untyped
9
+
10
+ def cancel: () -> untyped
11
+
12
+ def cancelled?: () -> untyped
13
+
14
+ def run: (?snapshot: untyped) -> untyped
15
+
16
+ private
17
+
18
+ def search_text: (untyped number) -> untyped
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,18 @@
1
+ # Generated from lib/vanken/app/stats_job.rb with RBS::Inline
2
+
3
+ module Vanken
4
+ module App
5
+ # Invoke refresh on the executor; cancel joins its current frame before store close.
6
+ class StatsJob
7
+ def initialize: (untyped document, ?specs: untyped, ?displayed_only: untyped) -> untyped
8
+
9
+ def refresh: () -> untyped
10
+
11
+ def cancel: () -> untyped
12
+
13
+ private
14
+
15
+ def reset: () -> untyped
16
+ end
17
+ end
18
+ end
@@ -13,6 +13,12 @@ module Vanken
13
13
 
14
14
  attr_reader flush_interval: untyped
15
15
 
16
+ attr_reader stop_count: untyped
17
+
18
+ attr_reader stop_duration: untyped
19
+
20
+ attr_reader stop_bytes: untyped
21
+
16
22
  def self.parse: (untyped argv) -> untyped
17
23
 
18
24
  def initialize: () -> untyped