vanken 0.1.0 → 0.2.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 +15 -0
- data/README.md +26 -11
- data/data/coloring_rules.yml +67 -0
- data/docs/performance.md +10 -0
- data/docs/releases.md +2 -4
- data/docs/usage.md +51 -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/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/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/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 +51 -3
|
@@ -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
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../gateway/statistics"
|
|
4
|
+
|
|
5
|
+
module Vanken
|
|
6
|
+
module App
|
|
7
|
+
# Invoke refresh on the executor; cancel joins its current frame before store close.
|
|
8
|
+
class StatsJob
|
|
9
|
+
def initialize(document, specs: ["phs"], displayed_only: false)
|
|
10
|
+
@document, @specs, @displayed_only = document, specs, displayed_only
|
|
11
|
+
@mutex, @numbers, @cancelled = Mutex.new, [], false
|
|
12
|
+
reset
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def refresh
|
|
16
|
+
@mutex.synchronize do
|
|
17
|
+
return [] if @cancelled || @document.closing?
|
|
18
|
+
numbers = @displayed_only ? @document.display_numbers.sort : (1..@document.count).to_a
|
|
19
|
+
reset unless numbers.take(@numbers.size) == @numbers
|
|
20
|
+
numbers.drop(@numbers.size).each do |number|
|
|
21
|
+
break if @cancelled || @document.closing?
|
|
22
|
+
@statistics.update(@dissector.dissect(@document.store.read(number)))
|
|
23
|
+
@numbers << number
|
|
24
|
+
end
|
|
25
|
+
@statistics.tables
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def cancel
|
|
30
|
+
@cancelled = true
|
|
31
|
+
@mutex.synchronize { @statistics.close }
|
|
32
|
+
self
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def reset
|
|
38
|
+
@statistics&.close
|
|
39
|
+
@numbers = []
|
|
40
|
+
@dissector = Gateway::Dissector.new(**@document.analysis_gateway_options)
|
|
41
|
+
@statistics = Gateway::Statistics.new(specs: @specs, registry: @dissector.registry, **@document.analysis_options)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -11,7 +11,7 @@ module Vanken
|
|
|
11
11
|
def run
|
|
12
12
|
number = 1
|
|
13
13
|
loop do
|
|
14
|
-
break if @document.
|
|
14
|
+
break if @document.analysis_stopped?
|
|
15
15
|
if number > @document.store.durable_count
|
|
16
16
|
break if @document.received? && number > @document.store.durable_count
|
|
17
17
|
@document.wait_for_frames
|
|
@@ -20,11 +20,13 @@ module Vanken
|
|
|
20
20
|
frame = @document.store.read(number)
|
|
21
21
|
begin
|
|
22
22
|
packet = @dissector.dissect(frame)
|
|
23
|
+
break if @document.analysis_stopped?
|
|
23
24
|
@analysis.update(packet)
|
|
24
25
|
rescue StandardError => error
|
|
25
26
|
@document.publish_failure(number, error)
|
|
26
27
|
packet = nil
|
|
27
28
|
end
|
|
29
|
+
break if @document.analysis_stopped?
|
|
28
30
|
@document.publish(number, packet) if packet
|
|
29
31
|
number += 1
|
|
30
32
|
if number % 256 == 0
|
|
@@ -11,6 +11,7 @@ module Vanken
|
|
|
11
11
|
@document, @verify_checksums = document, verify_checksums
|
|
12
12
|
end
|
|
13
13
|
def run
|
|
14
|
+
return if @document.analysis_stopped?
|
|
14
15
|
unless Gem.win_platform?
|
|
15
16
|
raise Vanken::Error, "packet analysis must run without root privileges" if Process.uid.zero? || Process.euid.zero?
|
|
16
17
|
end
|
|
@@ -23,10 +24,11 @@ module Vanken
|
|
|
23
24
|
@pid = Process.spawn(*arguments, in: child_in, out: child_out, close_others: true)
|
|
24
25
|
child_in.close
|
|
25
26
|
child_out.close
|
|
26
|
-
request(command: :start, spool: @document.store.directory, verify_checksums: @verify_checksums, analysis_options: @document.analysis_options
|
|
27
|
+
request(command: :start, spool: @document.store.directory, verify_checksums: @verify_checksums, analysis_options: @document.analysis_options,
|
|
28
|
+
gateway_options: @document.analysis_gateway_options)
|
|
27
29
|
number = 1
|
|
28
30
|
loop do
|
|
29
|
-
break if @document.
|
|
31
|
+
break if @document.analysis_stopped?
|
|
30
32
|
limit = @document.store.durable_count
|
|
31
33
|
if number > limit
|
|
32
34
|
break if @document.received? && number > @document.store.durable_count
|
|
@@ -36,13 +38,14 @@ module Vanken
|
|
|
36
38
|
last = [number + 255, limit].min
|
|
37
39
|
configuration = @document.analysis_configuration(number, last)
|
|
38
40
|
batch = request(configuration.merge(command: :analyze, first: number, last: last, interfaces: @document.store.interfaces))
|
|
41
|
+
break if @document.analysis_stopped?
|
|
39
42
|
raise Vanken::Error, "analysis response range mismatch" unless batch[:first] == number && batch[:last] == last
|
|
40
43
|
@document.publish_analysis_batch(batch, self, configuration[:generation], context_token: configuration[:context_token])
|
|
41
44
|
number = last + 1
|
|
42
45
|
end
|
|
43
46
|
rescue StandardError
|
|
44
|
-
@document.cancel
|
|
45
|
-
raise unless @document.
|
|
47
|
+
@document.cancel unless @document.analysis_stopped?
|
|
48
|
+
raise unless @document.analysis_stopped?
|
|
46
49
|
ensure
|
|
47
50
|
child_in&.close unless child_in&.closed?
|
|
48
51
|
child_out&.close unless child_out&.closed?
|
|
@@ -54,7 +57,7 @@ module Vanken
|
|
|
54
57
|
end
|
|
55
58
|
def request(value)
|
|
56
59
|
AnalyzerWire.write(@input, value)
|
|
57
|
-
response = AnalyzerWire.read(@output, cancelled: -> { @document.
|
|
60
|
+
response = AnalyzerWire.read(@output, cancelled: -> { @document.analysis_stopped? })
|
|
58
61
|
raise Vanken::Error, response[:error] if response[:error]
|
|
59
62
|
response
|
|
60
63
|
end
|
|
@@ -82,7 +85,7 @@ module Vanken
|
|
|
82
85
|
end
|
|
83
86
|
sleep(0.005)
|
|
84
87
|
end
|
|
85
|
-
raise Vanken::Error, "analysis worker failed while stopping" unless status.success? || @document.
|
|
88
|
+
raise Vanken::Error, "analysis worker failed while stopping" unless status.success? || @document.analysis_stopped? || $!
|
|
86
89
|
rescue Errno::ECHILD, Errno::ESRCH
|
|
87
90
|
nil
|
|
88
91
|
end
|
|
@@ -57,11 +57,11 @@ module Vanken
|
|
|
57
57
|
|
|
58
58
|
def initialize(request)
|
|
59
59
|
@reader = Reader.new(request.fetch(:spool))
|
|
60
|
-
@dissector = Gateway::Dissector.new(verify_checksums: request.fetch(:verify_checksums))
|
|
60
|
+
@dissector = Gateway::Dissector.new(**request.fetch(:gateway_options, {verify_checksums: request.fetch(:verify_checksums)}))
|
|
61
61
|
@analysis = Gateway::Analysis.new(registry: @dissector.registry, **request.fetch(:analysis_options))
|
|
62
62
|
@columns = Core::ColumnStore.new
|
|
63
63
|
@annotations = Core::AnnotationStore.new(request.fetch(:spool), persist: false)
|
|
64
|
-
@catalog = Gateway::FieldCatalog.new
|
|
64
|
+
@catalog = Gateway::FieldCatalog.new(registry: @dissector.registry)
|
|
65
65
|
@seen_fields = @catalog.names.to_set
|
|
66
66
|
@context = Context.new(@reader)
|
|
67
67
|
@next = 1
|
|
@@ -8,6 +8,7 @@ require_relative "../core/frame_store"
|
|
|
8
8
|
require_relative "../core/stores"
|
|
9
9
|
require_relative "../core/display_filter/compiler"
|
|
10
10
|
require_relative "../gateway/dissector"
|
|
11
|
+
require_relative "../gateway/display_capture_filter"
|
|
11
12
|
|
|
12
13
|
module Vanken
|
|
13
14
|
module Capture
|
|
@@ -153,12 +154,17 @@ module Vanken
|
|
|
153
154
|
raise ArgumentError, "filter chunk exceeds stored frames" unless last <= reader.count + 1
|
|
154
155
|
|
|
155
156
|
dissector = Gateway::Dissector.new(decode_as: payload.fetch("decode_as", []), plugins: payload.fetch("plugins", []))
|
|
156
|
-
program = Core::DisplayFilter.compile(payload.fetch("expr"), catalog: Gateway::FieldCatalog.new)
|
|
157
|
+
program = Core::DisplayFilter.compile(payload.fetch("expr"), catalog: Gateway::FieldCatalog.new(registry: dissector.registry))
|
|
158
|
+
capture = Gateway::DisplayCaptureFilter.new(payload.fetch("expr")) if payload.fetch("plugins", []).empty? && payload.fetch("decode_as", []).empty?
|
|
159
|
+
capture = nil unless capture&.expression
|
|
157
160
|
context = {marked: Set.new(payload.fetch("marked", [])), ignored: Set.new(payload.fetch("ignored", [])),
|
|
158
161
|
reference: payload.fetch("time_reference_ns") { reader.count.zero? ? 0 : reader.metadata(1)[:timestamp_ns] },
|
|
159
162
|
references: payload.fetch("time_references_ns", {}).map { |number, timestamp| [Integer(number), timestamp] }.sort_by(&:first),
|
|
160
163
|
predecessors: payload.fetch("displayed_predecessors", {})}
|
|
161
|
-
matches = (first...last).select
|
|
164
|
+
matches = (first...last).select do |number|
|
|
165
|
+
matched = capture&.match(reader.frame(number))
|
|
166
|
+
matched.nil? ? program.match?(View.new(reader, number, dissector, context)) : matched
|
|
167
|
+
end
|
|
162
168
|
{"matches" => matches}
|
|
163
169
|
ensure
|
|
164
170
|
reader&.close
|
|
@@ -23,7 +23,7 @@ module Vanken
|
|
|
23
23
|
when :list_interfaces then @stdout.puts(JSON.generate(Gateway::Interfaces.list)); return 0
|
|
24
24
|
when :check then return check(options)
|
|
25
25
|
when :version then @stdout.puts("Vanken #{VERSION}"); return 0
|
|
26
|
-
when :help then @stdout.puts("vanken-capture -i INTERFACE [--filter EXPR] [--snaplen N] [--no-promiscuous]\n [--buffer-size BYTES] [--direction in|out|inout] [--backend auto|ring|socket|bpf]\n [--drop-to UID:GID] [--stats-interval SEC] [--flush-interval SEC]\n --list-interfaces | --check [--interface IF] | --version"); return 0
|
|
26
|
+
when :help then @stdout.puts("vanken-capture -i INTERFACE [--filter EXPR] [--snaplen N] [--no-promiscuous]\n [--buffer-size BYTES] [--direction in|out|inout] [--backend auto|ring|socket|bpf]\n [--drop-to UID:GID] [--stats-interval SEC] [--flush-interval SEC]\n [--stop-count N] [--stop-duration SEC] [--stop-bytes BYTES]\n --list-interfaces | --check [--interface IF] | --version"); return 0
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
capture(options)
|
|
@@ -64,11 +64,18 @@ module Vanken
|
|
|
64
64
|
end
|
|
65
65
|
@control.write(:started, interface: options.capture[:interface], linktype: source.linktype,
|
|
66
66
|
snaplen: options.capture[:snaplen], backend: source.backend, filter: options.capture[:filter], privileges_dropped: dropped)
|
|
67
|
-
flushed_at = stats_at = monotonic
|
|
67
|
+
started_at = flushed_at = stats_at = monotonic
|
|
68
|
+
captured = 0
|
|
68
69
|
until @stop_reason || source.stopped?
|
|
69
70
|
packet = source.next_packet(timeout: 0.05)
|
|
70
|
-
|
|
71
|
+
if packet
|
|
72
|
+
writer << packet
|
|
73
|
+
captured += 1
|
|
74
|
+
end
|
|
71
75
|
now = monotonic
|
|
76
|
+
@stop_reason ||= "count_limit" if options.stop_count && captured >= options.stop_count
|
|
77
|
+
@stop_reason ||= "duration_limit" if options.stop_duration && now - started_at >= options.stop_duration
|
|
78
|
+
@stop_reason ||= "bytes_limit" if options.stop_bytes && writer.bytes_written >= options.stop_bytes
|
|
72
79
|
if now - flushed_at >= options.flush_interval
|
|
73
80
|
writer.flush
|
|
74
81
|
flushed_at = now
|
|
@@ -5,7 +5,8 @@ require "optparse"
|
|
|
5
5
|
module Vanken
|
|
6
6
|
module Capture
|
|
7
7
|
class HelperOptions
|
|
8
|
-
attr_reader :command, :capture, :drop_to, :stats_interval, :flush_interval
|
|
8
|
+
attr_reader :command, :capture, :drop_to, :stats_interval, :flush_interval,
|
|
9
|
+
:stop_count, :stop_duration, :stop_bytes
|
|
9
10
|
|
|
10
11
|
def self.parse(argv)
|
|
11
12
|
new.parse(argv)
|
|
@@ -36,13 +37,16 @@ module Vanken
|
|
|
36
37
|
end
|
|
37
38
|
opts.on("--stats-interval SEC", Float) { |v| @stats_interval = bounded(v, 0.01..60, "stats-interval") }
|
|
38
39
|
opts.on("--flush-interval SEC", Float) { |v| @flush_interval = bounded(v, 0.001..1, "flush-interval") }
|
|
40
|
+
opts.on("--stop-count N", Integer) { |v| @stop_count = bounded(v, 1..9_223_372_036_854_775_807, "stop-count") }
|
|
41
|
+
opts.on("--stop-duration SEC", Float) { |v| @stop_duration = bounded(v, 0.001..31_536_000, "stop-duration") }
|
|
42
|
+
opts.on("--stop-bytes N", Integer) { |v| @stop_bytes = bounded(v, 1..9_223_372_036_854_775_807, "stop-bytes") }
|
|
39
43
|
opts.on("--list-interfaces") { modes << :list_interfaces }
|
|
40
44
|
opts.on("--check") { modes << :check }
|
|
41
45
|
opts.on("--version") { modes << :version }
|
|
42
46
|
opts.on("-h", "--help") { modes << :help }
|
|
43
47
|
end
|
|
44
48
|
# OptionParser accepts long-option abbreviations by default; privileged inputs must be exact.
|
|
45
|
-
known = %w[--interface --filter --snaplen --no-promiscuous --buffer-size --direction --backend --drop-to --stats-interval --flush-interval --list-interfaces --check --version --help]
|
|
49
|
+
known = %w[--interface --filter --snaplen --no-promiscuous --buffer-size --direction --backend --drop-to --stats-interval --flush-interval --stop-count --stop-duration --stop-bytes --list-interfaces --check --version --help]
|
|
46
50
|
argv.each do |arg|
|
|
47
51
|
raise ArgumentError, "unknown option: #{arg}" if arg.start_with?("--") && !known.include?(arg.split("=", 2).first)
|
|
48
52
|
end
|
|
@@ -171,6 +171,7 @@ module Vanken
|
|
|
171
171
|
def capture_argv(options)
|
|
172
172
|
options.flat_map do |key, value|
|
|
173
173
|
next [] if value.nil?
|
|
174
|
+
next [] if %i[stop_count stop_duration stop_bytes].include?(key.to_sym) && value == 0
|
|
174
175
|
next value ? [] : ["--no-promiscuous"] if key.to_sym == :promiscuous
|
|
175
176
|
|
|
176
177
|
["--#{key.to_s.tr('_', '-')}", value.to_s]
|