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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +24 -0
- data/README.md +28 -11
- data/data/coloring_rules.yml +67 -0
- data/docs/performance.md +53 -0
- data/docs/releases.md +2 -4
- data/docs/usage.md +51 -0
- data/exe/vanken-setup-permissions +6 -0
- data/lib/vanken/app/capture_controller.rb +66 -1
- data/lib/vanken/app/document.rb +102 -39
- data/lib/vanken/app/document_analysis.rb +75 -0
- data/lib/vanken/app/document_jobs.rb +52 -9
- data/lib/vanken/app/expert_info.rb +26 -0
- data/lib/vanken/app/io_graph.rb +47 -0
- data/lib/vanken/app/navigation.rb +77 -0
- data/lib/vanken/app/search_job.rb +62 -0
- data/lib/vanken/app/stats_job.rb +45 -0
- data/lib/vanken/capture/analyzer.rb +3 -1
- data/lib/vanken/capture/analyzer_process.rb +9 -6
- data/lib/vanken/capture/analyzer_worker.rb +2 -2
- data/lib/vanken/capture/filter_worker.rb +8 -2
- data/lib/vanken/capture/helper_main.rb +10 -3
- data/lib/vanken/capture/helper_options.rb +6 -2
- data/lib/vanken/capture/launcher.rb +1 -0
- data/lib/vanken/capture/permission_setup.rb +201 -0
- data/lib/vanken/config/analysis_settings.rb +58 -0
- data/lib/vanken/config/columns.rb +138 -0
- data/lib/vanken/config/messages.rb +105 -0
- data/lib/vanken/config/preferences.rb +34 -8
- data/lib/vanken/config/profiles.rb +74 -0
- data/lib/vanken/config/sessions.rb +69 -0
- data/lib/vanken/config/yaml_file.rb +36 -0
- data/lib/vanken/core/coloring.rb +86 -0
- data/lib/vanken/core/frame_store.rb +2 -2
- data/lib/vanken/core/stores.rb +12 -0
- data/lib/vanken/gateway/detail_builder.rb +1 -1
- data/lib/vanken/gateway/display_capture_filter.rb +102 -0
- data/lib/vanken/gateway/dissector.rb +31 -6
- data/lib/vanken/gateway/exporter.rb +125 -0
- data/lib/vanken/gateway/file_writer.rb +26 -2
- data/lib/vanken/gateway/interfaces.rb +59 -0
- data/lib/vanken/gateway/resolver.rb +131 -0
- data/lib/vanken/gateway/statistics.rb +83 -0
- data/lib/vanken/gateway/stream.rb +159 -0
- data/lib/vanken/ui/actions.rb +54 -28
- data/lib/vanken/ui/analysis_dialogs.rb +371 -0
- data/lib/vanken/ui/application.rb +183 -9
- data/lib/vanken/ui/coloring_operations.rb +104 -0
- data/lib/vanken/ui/column_operations.rb +115 -0
- data/lib/vanken/ui/dialogs.rb +41 -26
- data/lib/vanken/ui/file_operations.rb +17 -12
- data/lib/vanken/ui/filter_operations.rb +43 -15
- data/lib/vanken/ui/main_view.rb +74 -29
- data/lib/vanken/ui/navigation_operations.rb +86 -0
- data/lib/vanken/ui/packet_source.rb +33 -1
- data/lib/vanken/ui/selection.rb +8 -2
- data/lib/vanken/ui/settings_operations.rb +233 -0
- data/lib/vanken/version.rb +1 -1
- data/packaging/README.md +41 -1
- data/packaging/linux/org.vanken.capture.policy +14 -0
- data/packaging/linux/vanken-capture.wrapper +6 -0
- data/packaging/linux/vanken.desktop +9 -0
- data/packaging/linux/vanken.sudoers +2 -0
- data/packaging/macos/chmod-bpf +6 -0
- data/packaging/macos/org.vanken.chmod-bpf.plist +8 -0
- data/sig/generated/vanken/app/capture_controller.rbs +16 -0
- data/sig/generated/vanken/app/document.rbs +23 -1
- data/sig/generated/vanken/app/document_analysis.rbs +21 -0
- data/sig/generated/vanken/app/document_jobs.rbs +6 -0
- data/sig/generated/vanken/app/expert_info.rbs +30 -0
- data/sig/generated/vanken/app/io_graph.rbs +45 -0
- data/sig/generated/vanken/app/navigation.rbs +31 -0
- data/sig/generated/vanken/app/search_job.rbs +21 -0
- data/sig/generated/vanken/app/stats_job.rbs +18 -0
- data/sig/generated/vanken/capture/helper_options.rbs +6 -0
- data/sig/generated/vanken/capture/permission_setup.rbs +43 -0
- data/sig/generated/vanken/config/analysis_settings.rbs +31 -0
- data/sig/generated/vanken/config/columns.rbs +37 -0
- data/sig/generated/vanken/config/messages.rbs +14 -0
- data/sig/generated/vanken/config/preferences.rbs +1 -1
- data/sig/generated/vanken/config/profiles.rbs +29 -0
- data/sig/generated/vanken/config/sessions.rbs +17 -0
- data/sig/generated/vanken/config/yaml_file.rbs +11 -0
- data/sig/generated/vanken/core/coloring.rbs +52 -0
- data/sig/generated/vanken/core/stores.rbs +4 -0
- data/sig/generated/vanken/gateway/display_capture_filter.rbs +37 -0
- data/sig/generated/vanken/gateway/dissector.rbs +15 -2
- data/sig/generated/vanken/gateway/exporter.rbs +27 -0
- data/sig/generated/vanken/gateway/file_writer.rbs +3 -0
- data/sig/generated/vanken/gateway/interfaces.rbs +12 -0
- data/sig/generated/vanken/gateway/resolver.rbs +52 -0
- data/sig/generated/vanken/gateway/statistics.rbs +104 -0
- data/sig/generated/vanken/gateway/stream.rbs +75 -0
- data/sig/generated/vanken/ui/analysis_dialogs.rbs +71 -0
- data/sig/generated/vanken/ui/application.rbs +23 -1
- data/sig/generated/vanken/ui/coloring_operations.rbs +29 -0
- data/sig/generated/vanken/ui/column_operations.rbs +29 -0
- data/sig/generated/vanken/ui/dialogs.rbs +5 -3
- data/sig/generated/vanken/ui/file_operations.rbs +2 -0
- data/sig/generated/vanken/ui/filter_operations.rbs +3 -1
- data/sig/generated/vanken/ui/main_view.rbs +14 -2
- data/sig/generated/vanken/ui/navigation_operations.rbs +43 -0
- data/sig/generated/vanken/ui/packet_source.rbs +4 -0
- data/sig/generated/vanken/ui/settings_operations.rbs +41 -0
- 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
|
data/lib/vanken/version.rb
CHANGED
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,
|
|
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,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
|