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
data/lib/vanken/app/document.rb
CHANGED
|
@@ -9,27 +9,36 @@ require_relative "../capture/receiver"
|
|
|
9
9
|
require_relative "../capture/analyzer"
|
|
10
10
|
require_relative "../capture/analyzer_process"
|
|
11
11
|
require_relative "document_jobs"
|
|
12
|
+
require_relative "../config/analysis_settings"
|
|
13
|
+
require_relative "document_analysis"
|
|
14
|
+
require_relative "navigation"
|
|
15
|
+
require_relative "../config/columns"
|
|
12
16
|
|
|
13
17
|
module Vanken
|
|
14
18
|
module App
|
|
15
19
|
class Document
|
|
16
20
|
include DocumentJobs
|
|
21
|
+
include DocumentAnalysis
|
|
22
|
+
include Navigation
|
|
17
23
|
attr_reader :store, :columns, :annotations, :error, :path, :source, :progress, :filter,
|
|
18
|
-
:marked, :ignored, :time_references, :catalog, :analysis_options
|
|
24
|
+
:marked, :ignored, :time_references, :catalog, :analysis_options, :analysis_gateway_options
|
|
19
25
|
attr_accessor :frame_latency, :scanner
|
|
20
26
|
|
|
21
|
-
def initialize(on_update: nil, preferences: nil, store: nil, scanner: nil, process_analysis: true)
|
|
27
|
+
def initialize(on_update: nil, preferences: nil, store: nil, scanner: nil, process_analysis: true, decode_as: nil, plugins: nil)
|
|
28
|
+
@verify_checksums = preferences&.get("analysis.verify_checksums") || false
|
|
29
|
+
settings = preferences && Config::AnalysisSettings.new(directory: preferences.directory)
|
|
30
|
+
@analysis_gateway_options = {verify_checksums: @verify_checksums, decode_as: decode_as || settings&.decode_as || [], plugins: plugins || settings&.plugins || []}.freeze
|
|
31
|
+
@dissector = Gateway::Dissector.new(**@analysis_gateway_options)
|
|
22
32
|
@store = store || Core::FrameStore.new
|
|
23
33
|
@columns = Core::ColumnStore.new
|
|
24
34
|
@annotations = Core::AnnotationStore.new(@store.directory)
|
|
25
35
|
@cache = Core::RowCache.new(limit: preferences&.get("packet_list.row_cache_rows") || 20_000)
|
|
26
|
-
@verify_checksums = preferences&.get("analysis.verify_checksums") || false
|
|
27
|
-
@dissector = Gateway::Dissector.new(verify_checksums: @verify_checksums)
|
|
28
36
|
@process_analysis = process_analysis
|
|
29
37
|
@analysis_options = {max_state_bytes: (preferences&.get("analysis.max_state_mib") || 256) << 20,
|
|
30
38
|
max_flows: preferences&.get("analysis.max_flows") || 100_000}.freeze
|
|
31
|
-
@catalog = Gateway::FieldCatalog.new
|
|
39
|
+
@catalog = Gateway::FieldCatalog.new(registry: @dissector.registry)
|
|
32
40
|
@mutex, @condition = Mutex.new, ConditionVariable.new
|
|
41
|
+
@close_mutex = Mutex.new
|
|
33
42
|
@jobs, @count, @generation = [], 0, 0
|
|
34
43
|
@display = nil
|
|
35
44
|
@marked, @ignored, @time_references = Set.new, Set.new, Set.new
|
|
@@ -38,15 +47,7 @@ module Vanken
|
|
|
38
47
|
@scanner = scanner
|
|
39
48
|
@scan_concurrency = preferences&.get("analysis.workers") || 4
|
|
40
49
|
@filter_tasks = []
|
|
41
|
-
|
|
42
|
-
@detail_reader = File.open(File.join(@store.directory, "details.jsonl"), "rb")
|
|
43
|
-
@detail_index = File.open(File.join(@store.directory, "reassembled.idx"), "w+b", 0o600)
|
|
44
|
-
@detail_offsets = {}
|
|
45
|
-
@summaries = File.open(File.join(@store.directory, "summaries.bin"), "w+b", 0o600)
|
|
46
|
-
@summary_index = File.open(File.join(@store.directory, "summaries.idx"), "w+b", 0o600)
|
|
47
|
-
@summary_reader = File.open(File.join(@store.directory, "summaries.bin"), "rb")
|
|
48
|
-
@summary_index_reader = File.open(File.join(@store.directory, "summaries.idx"), "rb")
|
|
49
|
-
@summary_count = 0
|
|
50
|
+
open_analysis_files
|
|
50
51
|
@frame_latency = 0.0
|
|
51
52
|
end
|
|
52
53
|
|
|
@@ -75,12 +76,18 @@ module Vanken
|
|
|
75
76
|
def displayed_count = @mutex.synchronize { @display ? @display.size : @count }
|
|
76
77
|
def number_at(index) = @mutex.synchronize { @display ? @display.fetch(index) : (index + 1).tap { |number| raise IndexError unless number.between?(1, @count) } }
|
|
77
78
|
def display_numbers = @mutex.synchronize { @display ? @display.dup : (1..@count).to_a }
|
|
78
|
-
def dirty? = @dirty
|
|
79
|
+
def dirty? = @dirty || (@source == :live && @store.count > (@saved_count || 0))
|
|
79
80
|
def loading? = !@analyzed && !@cancelled
|
|
80
81
|
def cancelled? = @cancelled
|
|
81
82
|
def received? = @received
|
|
82
83
|
def complete? = @analyzed
|
|
83
84
|
def closing? = !!@closing
|
|
85
|
+
def analysis_stopped? = closing? || !!@reanalysis_cancelled
|
|
86
|
+
def capture_stats
|
|
87
|
+
value = @reader.respond_to?(:stats) && @reader.stats
|
|
88
|
+
value.respond_to?(:to_h) ? value.to_h : {}
|
|
89
|
+
end
|
|
90
|
+
def expert_summary = @mutex.synchronize { {count: @annotations.expert_count, severity: @annotations.expert_max} }
|
|
84
91
|
def stream_frames(stream) = @mutex.synchronize { @annotations.streams[stream].dup }
|
|
85
92
|
def packet(number) = @dissector.dissect(@store.read(number))
|
|
86
93
|
# @rbs (Integer number) -> Gateway::PacketSnapshot?
|
|
@@ -93,7 +100,33 @@ module Vanken
|
|
|
93
100
|
base = @mutex.synchronize { @columns[number] }
|
|
94
101
|
frame = @store.metadata(number)
|
|
95
102
|
info = @cache.fetch(number) { persisted_summary(number) || persisted_details(number)&.fetch("info", nil) || packet(number).info }
|
|
96
|
-
base.merge(no: number, number: number, timestamp_ns: frame[:timestamp_ns], length: frame[:original_length], info: info)
|
|
103
|
+
base.merge(no: number, number: number, timestamp_ns: frame[:timestamp_ns], length: frame[:original_length], info: info).merge(custom_row(number))
|
|
104
|
+
end
|
|
105
|
+
def custom_columns = @custom_columns || []
|
|
106
|
+
def custom_columns=(columns)
|
|
107
|
+
raise Vanken::ConfigError, "invalid custom columns" unless columns.is_a?(Array) && columns.size <= 64 && columns.all? do |item|
|
|
108
|
+
item.is_a?(Hash) && Config::Columns.valid_field?(item["field"]) && item["key"] == "field:#{item['field']}" && [true, false].include?(item["visible"])
|
|
109
|
+
end
|
|
110
|
+
saved = columns.map { |item| item.slice("key", "field", "visible").transform_values { |value| value.is_a?(String) ? value.dup.freeze : value }.freeze }.freeze
|
|
111
|
+
@mutex.synchronize do
|
|
112
|
+
unless saved == @custom_columns
|
|
113
|
+
@custom_columns = saved
|
|
114
|
+
@sort_generation = (@sort_generation || 0) + 1
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
def custom_row(number)
|
|
119
|
+
fields = custom_columns.select { |item| item["visible"] }
|
|
120
|
+
return {} if fields.empty?
|
|
121
|
+
packet = view(number)
|
|
122
|
+
resolver = Core::DisplayFilter::FieldResolver.new(@catalog)
|
|
123
|
+
fields.to_h do |item|
|
|
124
|
+
field = item.fetch("field")
|
|
125
|
+
[item.fetch("key").to_sym, Config::Columns.display(packet.values(field), type: resolver.resolve(field).type)]
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
def custom_sort_value(number, field)
|
|
129
|
+
Config::Columns.sort_value(view(number).values(field).first, type: Core::DisplayFilter::FieldResolver.new(@catalog).resolve(field).type)
|
|
97
130
|
end
|
|
98
131
|
def details(number)
|
|
99
132
|
saved = persisted_details(number)
|
|
@@ -112,7 +145,7 @@ module Vanken
|
|
|
112
145
|
program, generation, snapshot = @mutex.synchronize { [@filter, @generation, @filter_context] }
|
|
113
146
|
matched = !program || program.match?(view(number, packet: packet, snapshot: snapshot))
|
|
114
147
|
committed = @mutex.synchronize do
|
|
115
|
-
next false if generation != @generation
|
|
148
|
+
next false if generation != @generation || !snapshot.equal?(@filter_context)
|
|
116
149
|
committing = true
|
|
117
150
|
@columns.append(columns)
|
|
118
151
|
@annotations.append(number, annotation)
|
|
@@ -123,8 +156,8 @@ module Vanken
|
|
|
123
156
|
@detail_index.write([number, @detail_offsets[number]].pack("Q<Q<"))
|
|
124
157
|
@detail_index.flush
|
|
125
158
|
end
|
|
159
|
+
record_live_predecessors(number, number, matched ? [number] : [], context: snapshot)
|
|
126
160
|
@count = number
|
|
127
|
-
@dirty = true if @source == :live
|
|
128
161
|
@display << number if @display && matched
|
|
129
162
|
true
|
|
130
163
|
end
|
|
@@ -142,6 +175,7 @@ module Vanken
|
|
|
142
175
|
@columns.append(source: "", destination: "", protocol: "Malformed", src_port: -1, dst_port: -1, ip_proto: -1, layers: [])
|
|
143
176
|
@annotations.append(number, tcp_stream: -1, seq_rel: -1, ack_rel: -1, analysis_flags: [], expert_max: 3,
|
|
144
177
|
expert_items: [{severity: :error, code: "vanken.internal_error", protocol: "vanken", message: error.message}], extra: {})
|
|
178
|
+
record_live_predecessors(number, number, [])
|
|
145
179
|
@count = number
|
|
146
180
|
@display << number if @display && !@filter
|
|
147
181
|
end
|
|
@@ -149,14 +183,20 @@ module Vanken
|
|
|
149
183
|
end
|
|
150
184
|
# Only immutable context for this unpublished range crosses the worker pipe.
|
|
151
185
|
def analysis_configuration(first, last)
|
|
186
|
+
raise Vanken::Error, "invalid analysis range" unless first.is_a?(Integer) && last.is_a?(Integer) && first.positive? && (last - first + 1).between?(1, 256)
|
|
152
187
|
@mutex.synchronize do
|
|
153
188
|
historical = !!@filter_context
|
|
154
189
|
current = @filter_context || {marked: @marked, ignored: @ignored, references: @time_references,
|
|
155
190
|
predecessors: nil, limit: @count, last_displayed: @display ? @display.last : @count}
|
|
156
|
-
|
|
191
|
+
predecessors = if current[:live_predecessors]
|
|
192
|
+
(first..last).to_h { |number| [number, displayed_predecessor(number, current)] }
|
|
193
|
+
else
|
|
194
|
+
current[:predecessors]&.slice(*(first..last).to_a)
|
|
195
|
+
end
|
|
196
|
+
snapshot = current.except(:live_predecessors, :live_predecessor_start).merge(marked: current[:marked].select { |n| n.between?(first, last) }.to_set,
|
|
157
197
|
ignored: current[:ignored].select { |n| n.between?(first, last) }.to_set,
|
|
158
|
-
references: current[:references].dup, predecessors:
|
|
159
|
-
unfiltered_predecessor: historical && current[:predecessors].nil?)
|
|
198
|
+
references: current[:references].dup, predecessors: predecessors,
|
|
199
|
+
unfiltered_predecessor: historical && current[:predecessors].nil? && !current[:live_predecessors])
|
|
160
200
|
{expression: @filter&.expression || "", generation: @generation, snapshot: snapshot,
|
|
161
201
|
historical_context: historical, context_token: @filter_context&.object_id}
|
|
162
202
|
end
|
|
@@ -185,8 +225,8 @@ module Vanken
|
|
|
185
225
|
@summary_index.write(offsets)
|
|
186
226
|
[@annotations, @details, @detail_index, @summaries, @summary_index].each(&:flush)
|
|
187
227
|
@catalog.merge(batch[:fields])
|
|
228
|
+
record_live_predecessors(first, last, batch[:matches])
|
|
188
229
|
@summary_count = @count = last
|
|
189
|
-
@dirty = true if @source == :live
|
|
190
230
|
@display.concat(batch[:matches]) if @display
|
|
191
231
|
true
|
|
192
232
|
end
|
|
@@ -199,7 +239,18 @@ module Vanken
|
|
|
199
239
|
notify
|
|
200
240
|
end
|
|
201
241
|
def receiving_done = (@received = true; signal)
|
|
202
|
-
def analyzing_done
|
|
242
|
+
def analyzing_done
|
|
243
|
+
@annotations.flush
|
|
244
|
+
@mutex.synchronize do
|
|
245
|
+
@filter_context = nil if @reanalysis_context && @filter_context.equal?(@reanalysis_context)
|
|
246
|
+
@reanalysis_context = nil
|
|
247
|
+
@rebuilding_filter_basis = nil
|
|
248
|
+
@rebuilding = false
|
|
249
|
+
@reanalysis_cancelled = false
|
|
250
|
+
@analyzed = true
|
|
251
|
+
end
|
|
252
|
+
notify(force: true)
|
|
253
|
+
end
|
|
203
254
|
def signal = @mutex.synchronize { @condition.broadcast }
|
|
204
255
|
def wait_for_frames = @mutex.synchronize { @condition.wait(@mutex, 0.05) }
|
|
205
256
|
def fail(error) = (@error = error; notify(force: true))
|
|
@@ -227,6 +278,15 @@ module Vanken
|
|
|
227
278
|
signal
|
|
228
279
|
self
|
|
229
280
|
end
|
|
281
|
+
def cancel_reanalysis
|
|
282
|
+
@mutex.synchronize do
|
|
283
|
+
if @rebuilding
|
|
284
|
+
@reanalysis_cancelled = true
|
|
285
|
+
@condition.broadcast
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
self
|
|
289
|
+
end
|
|
230
290
|
def wait(timeout = nil)
|
|
231
291
|
deadline = timeout && (Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout)
|
|
232
292
|
@jobs.each do |job|
|
|
@@ -237,22 +297,25 @@ module Vanken
|
|
|
237
297
|
self
|
|
238
298
|
end
|
|
239
299
|
def close
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
300
|
+
@close_mutex.synchronize do
|
|
301
|
+
return self if @closed
|
|
302
|
+
@closing = true
|
|
303
|
+
cancel_search
|
|
304
|
+
cancel
|
|
305
|
+
cancel_scan
|
|
306
|
+
wait(3)
|
|
307
|
+
@annotations.close
|
|
308
|
+
@details.close
|
|
309
|
+
@detail_reader.close
|
|
310
|
+
@detail_index.close
|
|
311
|
+
@summaries.close
|
|
312
|
+
@summary_index.close
|
|
313
|
+
@summary_reader.close
|
|
314
|
+
@summary_index_reader.close
|
|
315
|
+
@store.close
|
|
316
|
+
@closed = true
|
|
317
|
+
self
|
|
318
|
+
end
|
|
256
319
|
end
|
|
257
320
|
|
|
258
321
|
private
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Vanken
|
|
4
|
+
module App
|
|
5
|
+
module DocumentAnalysis
|
|
6
|
+
def self.included(base) = base.extend(ClassMethods)
|
|
7
|
+
|
|
8
|
+
module ClassMethods
|
|
9
|
+
def recover(directory, **options)
|
|
10
|
+
store = Core::FrameStore.recover(directory)
|
|
11
|
+
new(store: store, **options).analyze_store
|
|
12
|
+
rescue StandardError
|
|
13
|
+
store&.close(remove: false)
|
|
14
|
+
raise
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def analyze_store
|
|
19
|
+
@source ||= :live
|
|
20
|
+
@dirty = @source == :live unless @rebuilding
|
|
21
|
+
@received, @analyzed, @cancelled = true, false, false
|
|
22
|
+
@analyzer = @process_analysis ? Capture::AnalyzerProcess.new(self, verify_checksums: @verify_checksums) : Capture::Analyzer.new(self, @dissector)
|
|
23
|
+
@jobs << thread { @analyzer.run }
|
|
24
|
+
self
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def reanalyze(decode_as: @analysis_gateway_options[:decode_as], plugins: @analysis_gateway_options[:plugins], preferences: nil)
|
|
28
|
+
raise Vanken::Error, "stop capture or finish loading before changing analysis" if loading? || closing?
|
|
29
|
+
options = @analysis_gateway_options.merge(decode_as: decode_as, plugins: plugins).freeze
|
|
30
|
+
options = options.merge(verify_checksums: preferences.get("analysis.verify_checksums")).freeze if preferences
|
|
31
|
+
dissector = Gateway::Dissector.new(**options)
|
|
32
|
+
snapshot = @mutex.synchronize { reanalysis_snapshot }
|
|
33
|
+
cancel_search if respond_to?(:cancel_search)
|
|
34
|
+
cancel_scan
|
|
35
|
+
wait(3)
|
|
36
|
+
@mutex.synchronize do
|
|
37
|
+
@analysis_gateway_options, @dissector = options, dissector
|
|
38
|
+
@verify_checksums = options[:verify_checksums]
|
|
39
|
+
if preferences
|
|
40
|
+
@analysis_options = {max_state_bytes: preferences.get("analysis.max_state_mib") << 20, max_flows: preferences.get("analysis.max_flows")}.freeze
|
|
41
|
+
@scan_concurrency = preferences.get("analysis.workers")
|
|
42
|
+
@cache = Core::RowCache.new(limit: preferences.get("packet_list.row_cache_rows"))
|
|
43
|
+
end
|
|
44
|
+
@columns = Core::ColumnStore.new
|
|
45
|
+
@annotations.close
|
|
46
|
+
@annotations = Core::AnnotationStore.new(@store.directory)
|
|
47
|
+
@cache.clear
|
|
48
|
+
@catalog = Gateway::FieldCatalog.new(registry: @dissector.registry)
|
|
49
|
+
@count, @display, @error = 0, (@filter ? [] : nil), nil
|
|
50
|
+
@rebuilding = true
|
|
51
|
+
@reanalysis_cancelled = false
|
|
52
|
+
@rebuilding_filter_basis = @filter_basis
|
|
53
|
+
@filter_context = @reanalysis_context = snapshot
|
|
54
|
+
[@details, @detail_reader, @detail_index, @summaries, @summary_index, @summary_reader, @summary_index_reader].each(&:close)
|
|
55
|
+
open_analysis_files
|
|
56
|
+
end
|
|
57
|
+
analyze_store
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def open_analysis_files
|
|
63
|
+
@details = File.open(File.join(@store.directory, "details.jsonl"), "w+b", 0o600)
|
|
64
|
+
@detail_reader = File.open(File.join(@store.directory, "details.jsonl"), "rb")
|
|
65
|
+
@detail_index = File.open(File.join(@store.directory, "reassembled.idx"), "w+b", 0o600)
|
|
66
|
+
@detail_offsets = {}
|
|
67
|
+
@summaries = File.open(File.join(@store.directory, "summaries.bin"), "w+b", 0o600)
|
|
68
|
+
@summary_index = File.open(File.join(@store.directory, "summaries.idx"), "w+b", 0o600)
|
|
69
|
+
@summary_reader = File.open(File.join(@store.directory, "summaries.bin"), "rb")
|
|
70
|
+
@summary_index_reader = File.open(File.join(@store.directory, "summaries.idx"), "rb")
|
|
71
|
+
@summary_count = 0
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -9,7 +9,8 @@ module Vanken
|
|
|
9
9
|
cancel_scan
|
|
10
10
|
if expression.empty?
|
|
11
11
|
@mutex.synchronize do
|
|
12
|
-
@filter = @filter_context = @display = @progress = @filter_job = nil
|
|
12
|
+
@filter = @filter_context = @filter_basis = @display = @progress = @filter_job = nil
|
|
13
|
+
@filter_live_predecessors = @filter_live_predecessor_start = nil
|
|
13
14
|
end
|
|
14
15
|
notify(force: true)
|
|
15
16
|
return self
|
|
@@ -18,6 +19,9 @@ module Vanken
|
|
|
18
19
|
context = filter_snapshot(displayed_delta: program.fields.include?("frame.time_delta_displayed"))
|
|
19
20
|
@filter = program
|
|
20
21
|
@filter_context = context
|
|
22
|
+
@filter_basis = context
|
|
23
|
+
@filter_live_predecessors = program.fields.include?("frame.time_delta_displayed") ? "".b : nil
|
|
24
|
+
@filter_live_predecessor_start = @filter_live_predecessors ? @count + 1 : nil
|
|
21
25
|
# Reserve the history's capacity before publishing matches without placeholder rows.
|
|
22
26
|
@display = Array.new(@count)
|
|
23
27
|
@display[0, @count] = []
|
|
@@ -88,6 +92,7 @@ module Vanken
|
|
|
88
92
|
end
|
|
89
93
|
|
|
90
94
|
def cancel_scan
|
|
95
|
+
cancel_search
|
|
91
96
|
tasks = @mutex.synchronize do
|
|
92
97
|
@generation += 1
|
|
93
98
|
@progress = nil
|
|
@@ -109,7 +114,7 @@ module Vanken
|
|
|
109
114
|
end
|
|
110
115
|
predecessors = (first...last).to_h { |number| [number.to_s, displayed_predecessor(number, snapshot)] }
|
|
111
116
|
{"spool" => @store.directory, "expr" => expression, "from" => first, "to" => last,
|
|
112
|
-
"decode_as" => [], "plugins" => [], "annotations_json" => snapshot[:annotations_json],
|
|
117
|
+
"decode_as" => @analysis_gateway_options[:decode_as], "plugins" => @analysis_gateway_options[:plugins], "annotations_json" => snapshot[:annotations_json],
|
|
113
118
|
"marked" => snapshot[:marked].select { |number| number >= first && number < last },
|
|
114
119
|
"ignored" => snapshot[:ignored].select { |number| number >= first && number < last },
|
|
115
120
|
"time_references_ns" => snapshot[:references].to_h { |number| [number.to_s, @store.metadata(number)[:timestamp_ns]] },
|
|
@@ -118,13 +123,19 @@ module Vanken
|
|
|
118
123
|
end
|
|
119
124
|
|
|
120
125
|
def displayed_predecessor(number, snapshot)
|
|
126
|
+
if (packed = snapshot[:live_predecessors]) && number >= snapshot[:live_predecessor_start]
|
|
127
|
+
offset = (number - snapshot[:live_predecessor_start]) * 8
|
|
128
|
+
return packed.unpack1("Q<", offset: offset) if offset < packed.bytesize
|
|
129
|
+
end
|
|
121
130
|
return number - 1 unless snapshot[:predecessors]
|
|
122
131
|
return snapshot[:last_displayed] || 0 if number > snapshot[:limit]
|
|
123
132
|
snapshot[:predecessors].fetch(number, 0)
|
|
124
133
|
end
|
|
125
134
|
|
|
126
135
|
def sort(key, direction = :asc)
|
|
136
|
+
cancel_search
|
|
127
137
|
raise ArgumentError, "invalid sort direction" unless %i[asc desc].include?(direction)
|
|
138
|
+
custom_field = custom_columns.find { |item| item["key"] == key.to_s }&.fetch("field")
|
|
128
139
|
generation, sort_generation, filter_job = @mutex.synchronize do
|
|
129
140
|
@sort_generation = (@sort_generation || 0) + 1
|
|
130
141
|
[@generation, @sort_generation, @filter_job]
|
|
@@ -134,11 +145,15 @@ module Vanken
|
|
|
134
145
|
next if generation != @generation || sort_generation != @sort_generation || @closing
|
|
135
146
|
numbers = display_numbers
|
|
136
147
|
values = numbers.to_h do |number|
|
|
137
|
-
value =
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
148
|
+
value = if custom_field
|
|
149
|
+
custom_sort_value(number, custom_field)
|
|
150
|
+
else
|
|
151
|
+
case key.to_sym
|
|
152
|
+
when :no, :number then number
|
|
153
|
+
when :time then @store.metadata(number)[:timestamp_ns]
|
|
154
|
+
when :length then @store.metadata(number)[:original_length]
|
|
155
|
+
else row(number).fetch(key.to_sym)
|
|
156
|
+
end
|
|
142
157
|
end
|
|
143
158
|
[number, value]
|
|
144
159
|
end
|
|
@@ -155,7 +170,7 @@ module Vanken
|
|
|
155
170
|
def save(path, format: nil, numbers: nil)
|
|
156
171
|
@error = nil
|
|
157
172
|
format ||= File.extname(path) == ".pcap" ? :pcap : :pcapng
|
|
158
|
-
limit =
|
|
173
|
+
limit = @store.durable_count
|
|
159
174
|
@save_job = thread do
|
|
160
175
|
temporary = Tempfile.create([".vanken-", ".tmp"], File.dirname(File.expand_path(path)))
|
|
161
176
|
begin
|
|
@@ -168,7 +183,8 @@ module Vanken
|
|
|
168
183
|
temporary.close
|
|
169
184
|
File.rename(temporary.path, File.expand_path(path))
|
|
170
185
|
@mutex.synchronize do
|
|
171
|
-
@
|
|
186
|
+
@saved_count = limit unless numbers
|
|
187
|
+
@dirty = false if !numbers && @store.count == limit
|
|
172
188
|
@path = File.expand_path(path) unless numbers
|
|
173
189
|
end
|
|
174
190
|
notify(force: true)
|
|
@@ -188,6 +204,33 @@ module Vanken
|
|
|
188
204
|
|
|
189
205
|
private
|
|
190
206
|
def stale_job?(generation) = @closing || generation != @generation
|
|
207
|
+
# Retain only post-filter arrivals, at eight bytes per packet, and only for
|
|
208
|
+
# displayed-delta filters. Rebuilds read this basis without replacing it.
|
|
209
|
+
def record_live_predecessors(first, last, matches, context: @filter_context)
|
|
210
|
+
return unless @filter_live_predecessors
|
|
211
|
+
return if @rebuilding && @filter_basis.equal?(@rebuilding_filter_basis)
|
|
212
|
+
previous = (@display ? @display.last : @count) || 0
|
|
213
|
+
cursor = 0
|
|
214
|
+
(first..last).each do |number|
|
|
215
|
+
if number >= @filter_live_predecessor_start
|
|
216
|
+
raise Vanken::Error, "nonsequential displayed-delta history" unless number == @filter_live_predecessor_start + (@filter_live_predecessors.bytesize / 8)
|
|
217
|
+
predecessor = context ? displayed_predecessor(number, context) : previous
|
|
218
|
+
@filter_live_predecessors << [predecessor].pack("Q<")
|
|
219
|
+
end
|
|
220
|
+
if matches[cursor] == number
|
|
221
|
+
previous = number
|
|
222
|
+
cursor += 1
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def reanalysis_snapshot
|
|
228
|
+
basis = @filter_basis || filter_snapshot(displayed_delta: !!@filter&.fields&.include?("frame.time_delta_displayed"))
|
|
229
|
+
return basis unless @filter_live_predecessors
|
|
230
|
+
basis.merge(live_predecessors: @filter_live_predecessors.dup.freeze,
|
|
231
|
+
live_predecessor_start: @filter_live_predecessor_start, limit: @count).freeze
|
|
232
|
+
end
|
|
233
|
+
|
|
191
234
|
def filter_snapshot(displayed_delta: true)
|
|
192
235
|
predecessors = @display.each_with_index.to_h { |number, index| [number, index.zero? ? 0 : @display[index - 1]] }.freeze if displayed_delta && @display
|
|
193
236
|
{marked: @marked.dup.freeze, ignored: @ignored.dup.freeze, references: @time_references.dup.freeze,
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Vanken
|
|
4
|
+
module App
|
|
5
|
+
ExpertRow = Data.define(:severity, :code, :protocol, :message, :count, :numbers)
|
|
6
|
+
|
|
7
|
+
module ExpertInfo
|
|
8
|
+
def self.rows(document, displayed_only: false)
|
|
9
|
+
displayed = document.display_numbers.to_set if displayed_only
|
|
10
|
+
groups = {}
|
|
11
|
+
document.annotations.experts.dup.each do |number, items|
|
|
12
|
+
next if displayed && !displayed.include?(number)
|
|
13
|
+
items.each do |item|
|
|
14
|
+
key = [Gateway::Severity.normalize(item[:severity]), item[:code], item[:protocol]]
|
|
15
|
+
group = (groups[key] ||= {message: item[:message], count: 0, numbers: []})
|
|
16
|
+
group[:count] += 1
|
|
17
|
+
group[:numbers] << number unless group[:numbers].last == number
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
groups.map do |(severity, code, protocol), value|
|
|
21
|
+
ExpertRow.new(severity: severity, code: code, protocol: protocol, **value.merge(numbers: value[:numbers].freeze))
|
|
22
|
+
end.sort_by { |row| [-Gateway::Severity.rank(row.severity), row.protocol, row.code] }
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../core/display_filter/compiler"
|
|
4
|
+
|
|
5
|
+
module Vanken
|
|
6
|
+
module App
|
|
7
|
+
IOPoint = Data.define(:interval, :start_ns, :packets, :bytes)
|
|
8
|
+
IOSeries = Data.define(:expression, :interval_ns, :points, :omitted_intervals)
|
|
9
|
+
|
|
10
|
+
module IOGraph
|
|
11
|
+
INTERVALS = [0.01, 0.1, 1, 10, 60].freeze
|
|
12
|
+
|
|
13
|
+
def self.build(document, interval: 1, series: [""], displayed_only: false, cancelled: nil)
|
|
14
|
+
raise ArgumentError, "invalid I/O interval" unless INTERVALS.include?(interval)
|
|
15
|
+
raise ArgumentError, "I/O graph needs 1-64 series" unless series.size.between?(1, 64) && series.all? { |expression| expression.is_a?(String) }
|
|
16
|
+
interval_ns = (interval * 1_000_000_000).to_i
|
|
17
|
+
programs = series.map { |expression| Core::DisplayFilter.compile(expression, catalog: document.catalog) }
|
|
18
|
+
numbers = displayed_only ? document.display_numbers : (1..document.count)
|
|
19
|
+
start = document.count.zero? ? 0 : document.store.metadata(1)[:timestamp_ns]
|
|
20
|
+
bins = series.map { {} }
|
|
21
|
+
last = 0
|
|
22
|
+
numbers.each do |number|
|
|
23
|
+
raise Vanken::Error, "operation cancelled" if document.closing? || cancelled&.call
|
|
24
|
+
metadata = document.store.metadata(number)
|
|
25
|
+
index = [(metadata[:timestamp_ns] - start) / interval_ns, 0].max
|
|
26
|
+
last = [last, index].max
|
|
27
|
+
view = nil
|
|
28
|
+
programs.each_with_index do |program, position|
|
|
29
|
+
next unless series[position].empty? || program.match?(view ||= document.view(number))
|
|
30
|
+
row = (bins[position][index] ||= [0, 0])
|
|
31
|
+
row[0] += 1
|
|
32
|
+
row[1] += metadata[:original_length]
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
# ponytail: retain at most 100,000 intervals; coarsen the selected interval for longer captures.
|
|
36
|
+
first = [last - 99_999, 0].max
|
|
37
|
+
series.each_with_index.map do |expression, position|
|
|
38
|
+
points = (document.count.zero? ? [] : (first..last)).map do |index|
|
|
39
|
+
packets, bytes = bins[position].fetch(index, [0, 0])
|
|
40
|
+
IOPoint.new(interval: index, start_ns: start + (index * interval_ns), packets: packets, bytes: bytes)
|
|
41
|
+
end
|
|
42
|
+
IOSeries.new(expression: expression, interval_ns: interval_ns, points: points.freeze, omitted_intervals: first)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "search_job"
|
|
4
|
+
|
|
5
|
+
module Vanken
|
|
6
|
+
module App
|
|
7
|
+
module Navigation
|
|
8
|
+
def displayed_index(number)
|
|
9
|
+
@mutex.synchronize { @display ? @display.index(number) : (number.is_a?(Integer) && number.between?(1, @count) ? number - 1 : nil) }
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def toggle_mark(number) = toggle_packet_state(@marked, number, "frame.marked")
|
|
13
|
+
def toggle_ignore(number) = toggle_packet_state(@ignored, number, "frame.ignored")
|
|
14
|
+
def toggle_time_reference(number) = toggle_packet_state(@time_references, number, "frame.time_relative")
|
|
15
|
+
def mark_all_displayed
|
|
16
|
+
@mutex.synchronize { @marked.merge(@display || (1..@count)) }
|
|
17
|
+
packet_states_changed("frame.marked")
|
|
18
|
+
self
|
|
19
|
+
end
|
|
20
|
+
def unmark_all
|
|
21
|
+
@mutex.synchronize { @marked.clear }
|
|
22
|
+
packet_states_changed("frame.marked")
|
|
23
|
+
self
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def conversation_neighbor(number, direction)
|
|
27
|
+
return nil unless number.is_a?(Integer) && [1, -1].include?(direction)
|
|
28
|
+
@mutex.synchronize do
|
|
29
|
+
return nil unless number.between?(1, @count)
|
|
30
|
+
stream = @annotations[number][:tcp_stream]
|
|
31
|
+
return nil if stream.negative?
|
|
32
|
+
numbers = @annotations.streams.fetch(stream, [])
|
|
33
|
+
index = numbers.bsearch_index { |item| item >= number } || numbers.size
|
|
34
|
+
index += direction
|
|
35
|
+
while index.between?(0, numbers.size - 1)
|
|
36
|
+
candidate = numbers[index]
|
|
37
|
+
return candidate unless @display && !@display.include?(candidate)
|
|
38
|
+
index += direction
|
|
39
|
+
end
|
|
40
|
+
nil
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def search(query, **options, &finished)
|
|
45
|
+
cancel_search
|
|
46
|
+
job = SearchJob.new(self, query, **options)
|
|
47
|
+
@search_job = job
|
|
48
|
+
snapshot = @mutex.synchronize { filter_snapshot(displayed_delta: !!job.program&.fields&.include?("frame.time_delta_displayed")) }
|
|
49
|
+
@jobs << thread do
|
|
50
|
+
hit = job.run(snapshot: snapshot)
|
|
51
|
+
finished&.call(hit, nil) unless job.cancelled?
|
|
52
|
+
rescue StandardError => error
|
|
53
|
+
finished&.call(nil, error) unless job.cancelled?
|
|
54
|
+
end
|
|
55
|
+
job
|
|
56
|
+
end
|
|
57
|
+
def cancel_search
|
|
58
|
+
@search_job&.cancel
|
|
59
|
+
@search_job = nil
|
|
60
|
+
self
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
def toggle_packet_state(values, number, field)
|
|
65
|
+
@mutex.synchronize do
|
|
66
|
+
raise Vanken::Error, "invalid packet number" unless number.is_a?(Integer) && number.between?(1, @count)
|
|
67
|
+
values.include?(number) ? values.delete(number) : values.add(number)
|
|
68
|
+
end
|
|
69
|
+
packet_states_changed(field)
|
|
70
|
+
self
|
|
71
|
+
end
|
|
72
|
+
def packet_states_changed(field)
|
|
73
|
+
@filter && @filter.fields.include?(field) ? apply_filter(@filter.expression) : notify(force: true)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../core/display_filter/compiler"
|
|
4
|
+
|
|
5
|
+
module Vanken
|
|
6
|
+
module App
|
|
7
|
+
class SearchJob
|
|
8
|
+
attr_reader :program
|
|
9
|
+
def initialize(document, query, mode: :filter, target: :bytes, from: nil, direction: 1)
|
|
10
|
+
@document, @mode, @target, @from = document, mode.to_sym, target.to_sym, from
|
|
11
|
+
raise Vanken::Error, "invalid search mode or target" unless %i[filter hex string regex].include?(@mode) && %i[list details bytes].include?(@target)
|
|
12
|
+
raise Vanken::Error, "search query is empty" if query.empty?
|
|
13
|
+
raise Vanken::Error, "invalid search direction" unless [1, -1].include?(direction)
|
|
14
|
+
@direction = direction
|
|
15
|
+
@program = Core::DisplayFilter.compile(query, catalog: document.catalog) if @mode == :filter
|
|
16
|
+
@query = if @mode == :hex
|
|
17
|
+
hex = query.delete(" \t\r\n:")
|
|
18
|
+
raise Vanken::Error, "16 進数は 2 桁ずつ入力してください" unless /\A(?:[\da-fA-F]{2})+\z/.match?(hex)
|
|
19
|
+
[hex].pack("H*")
|
|
20
|
+
elsif @mode == :regex
|
|
21
|
+
Regexp.new(@target == :bytes ? query.b : query, timeout: 0.1)
|
|
22
|
+
else
|
|
23
|
+
query.dup
|
|
24
|
+
end
|
|
25
|
+
rescue RegexpError => error
|
|
26
|
+
raise Vanken::Error, "invalid search pattern: #{error.message}"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def cancel = @cancelled = true
|
|
30
|
+
def cancelled? = @cancelled || @document.closing?
|
|
31
|
+
def run(snapshot: nil)
|
|
32
|
+
limit = @document.displayed_count
|
|
33
|
+
return nil if limit.zero?
|
|
34
|
+
origin = @document.displayed_index(@from) || (@direction.positive? ? -1 : limit)
|
|
35
|
+
1.upto(limit) do |step|
|
|
36
|
+
return nil if cancelled?
|
|
37
|
+
number = @document.number_at((origin + (step * @direction)) % limit)
|
|
38
|
+
matched = if @mode == :filter
|
|
39
|
+
@program.match?(@document.view(number, snapshot: snapshot))
|
|
40
|
+
else
|
|
41
|
+
text = search_text(number)
|
|
42
|
+
@mode == :regex ? @query.match?(text) : text.b.include?(@query.b)
|
|
43
|
+
end
|
|
44
|
+
return number if matched && !cancelled?
|
|
45
|
+
Thread.pass if step % 256 == 0
|
|
46
|
+
end
|
|
47
|
+
nil
|
|
48
|
+
rescue Regexp::TimeoutError
|
|
49
|
+
raise Vanken::Error, "検索の正規表現が時間制限を超えました"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
def search_text(number)
|
|
54
|
+
case @target
|
|
55
|
+
when :bytes then @document.store.read(number).bytes
|
|
56
|
+
when :list then @document.row(number).values.join("\t")
|
|
57
|
+
when :details then @document.details(number).flat_map(&:descendants).map(&:label).join("\n")
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|