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.
Files changed (95) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +15 -0
  3. data/README.md +26 -11
  4. data/data/coloring_rules.yml +67 -0
  5. data/docs/performance.md +10 -0
  6. data/docs/releases.md +2 -4
  7. data/docs/usage.md +51 -0
  8. data/lib/vanken/app/capture_controller.rb +66 -1
  9. data/lib/vanken/app/document.rb +102 -39
  10. data/lib/vanken/app/document_analysis.rb +75 -0
  11. data/lib/vanken/app/document_jobs.rb +52 -9
  12. data/lib/vanken/app/expert_info.rb +26 -0
  13. data/lib/vanken/app/io_graph.rb +47 -0
  14. data/lib/vanken/app/navigation.rb +77 -0
  15. data/lib/vanken/app/search_job.rb +62 -0
  16. data/lib/vanken/app/stats_job.rb +45 -0
  17. data/lib/vanken/capture/analyzer.rb +3 -1
  18. data/lib/vanken/capture/analyzer_process.rb +9 -6
  19. data/lib/vanken/capture/analyzer_worker.rb +2 -2
  20. data/lib/vanken/capture/filter_worker.rb +8 -2
  21. data/lib/vanken/capture/helper_main.rb +10 -3
  22. data/lib/vanken/capture/helper_options.rb +6 -2
  23. data/lib/vanken/capture/launcher.rb +1 -0
  24. data/lib/vanken/config/analysis_settings.rb +58 -0
  25. data/lib/vanken/config/columns.rb +138 -0
  26. data/lib/vanken/config/messages.rb +105 -0
  27. data/lib/vanken/config/preferences.rb +34 -8
  28. data/lib/vanken/config/profiles.rb +74 -0
  29. data/lib/vanken/config/sessions.rb +69 -0
  30. data/lib/vanken/config/yaml_file.rb +36 -0
  31. data/lib/vanken/core/coloring.rb +86 -0
  32. data/lib/vanken/core/frame_store.rb +2 -2
  33. data/lib/vanken/core/stores.rb +12 -0
  34. data/lib/vanken/gateway/detail_builder.rb +1 -1
  35. data/lib/vanken/gateway/display_capture_filter.rb +102 -0
  36. data/lib/vanken/gateway/dissector.rb +31 -6
  37. data/lib/vanken/gateway/exporter.rb +125 -0
  38. data/lib/vanken/gateway/file_writer.rb +26 -2
  39. data/lib/vanken/gateway/interfaces.rb +59 -0
  40. data/lib/vanken/gateway/resolver.rb +131 -0
  41. data/lib/vanken/gateway/statistics.rb +83 -0
  42. data/lib/vanken/gateway/stream.rb +159 -0
  43. data/lib/vanken/ui/actions.rb +54 -28
  44. data/lib/vanken/ui/analysis_dialogs.rb +371 -0
  45. data/lib/vanken/ui/application.rb +183 -9
  46. data/lib/vanken/ui/coloring_operations.rb +104 -0
  47. data/lib/vanken/ui/column_operations.rb +115 -0
  48. data/lib/vanken/ui/dialogs.rb +41 -26
  49. data/lib/vanken/ui/file_operations.rb +17 -12
  50. data/lib/vanken/ui/filter_operations.rb +43 -15
  51. data/lib/vanken/ui/main_view.rb +74 -29
  52. data/lib/vanken/ui/navigation_operations.rb +86 -0
  53. data/lib/vanken/ui/packet_source.rb +33 -1
  54. data/lib/vanken/ui/selection.rb +8 -2
  55. data/lib/vanken/ui/settings_operations.rb +233 -0
  56. data/lib/vanken/version.rb +1 -1
  57. data/sig/generated/vanken/app/capture_controller.rbs +16 -0
  58. data/sig/generated/vanken/app/document.rbs +23 -1
  59. data/sig/generated/vanken/app/document_analysis.rbs +21 -0
  60. data/sig/generated/vanken/app/document_jobs.rbs +6 -0
  61. data/sig/generated/vanken/app/expert_info.rbs +30 -0
  62. data/sig/generated/vanken/app/io_graph.rbs +45 -0
  63. data/sig/generated/vanken/app/navigation.rbs +31 -0
  64. data/sig/generated/vanken/app/search_job.rbs +21 -0
  65. data/sig/generated/vanken/app/stats_job.rbs +18 -0
  66. data/sig/generated/vanken/capture/helper_options.rbs +6 -0
  67. data/sig/generated/vanken/config/analysis_settings.rbs +31 -0
  68. data/sig/generated/vanken/config/columns.rbs +37 -0
  69. data/sig/generated/vanken/config/messages.rbs +14 -0
  70. data/sig/generated/vanken/config/preferences.rbs +1 -1
  71. data/sig/generated/vanken/config/profiles.rbs +29 -0
  72. data/sig/generated/vanken/config/sessions.rbs +17 -0
  73. data/sig/generated/vanken/config/yaml_file.rbs +11 -0
  74. data/sig/generated/vanken/core/coloring.rbs +52 -0
  75. data/sig/generated/vanken/core/stores.rbs +4 -0
  76. data/sig/generated/vanken/gateway/display_capture_filter.rbs +37 -0
  77. data/sig/generated/vanken/gateway/dissector.rbs +15 -2
  78. data/sig/generated/vanken/gateway/exporter.rbs +27 -0
  79. data/sig/generated/vanken/gateway/file_writer.rbs +3 -0
  80. data/sig/generated/vanken/gateway/interfaces.rbs +12 -0
  81. data/sig/generated/vanken/gateway/resolver.rbs +52 -0
  82. data/sig/generated/vanken/gateway/statistics.rbs +104 -0
  83. data/sig/generated/vanken/gateway/stream.rbs +75 -0
  84. data/sig/generated/vanken/ui/analysis_dialogs.rbs +71 -0
  85. data/sig/generated/vanken/ui/application.rbs +23 -1
  86. data/sig/generated/vanken/ui/coloring_operations.rbs +29 -0
  87. data/sig/generated/vanken/ui/column_operations.rbs +29 -0
  88. data/sig/generated/vanken/ui/dialogs.rbs +5 -3
  89. data/sig/generated/vanken/ui/file_operations.rbs +2 -0
  90. data/sig/generated/vanken/ui/filter_operations.rbs +3 -1
  91. data/sig/generated/vanken/ui/main_view.rbs +14 -2
  92. data/sig/generated/vanken/ui/navigation_operations.rbs +43 -0
  93. data/sig/generated/vanken/ui/packet_source.rbs +4 -0
  94. data/sig/generated/vanken/ui/settings_operations.rbs +41 -0
  95. 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 = case key.to_sym
138
- when :no, :number then number
139
- when :time then @store.metadata(number)[:timestamp_ns]
140
- when :length then @store.metadata(number)[:original_length]
141
- else row(number).fetch(key.to_sym)
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 = count
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
- @dirty = false if !numbers && @count == limit && @store.count == limit
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.respond_to?(:closing?) && @document.closing?
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.closing?
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.closing?
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.closing? })
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.closing? || $!
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 { |number| program.match?(View.new(reader, number, dissector, context)) }
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
- writer << packet if packet
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]