vanken 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +24 -0
  3. data/README.md +28 -11
  4. data/data/coloring_rules.yml +67 -0
  5. data/docs/performance.md +53 -0
  6. data/docs/releases.md +2 -4
  7. data/docs/usage.md +51 -0
  8. data/exe/vanken-setup-permissions +6 -0
  9. data/lib/vanken/app/capture_controller.rb +66 -1
  10. data/lib/vanken/app/document.rb +102 -39
  11. data/lib/vanken/app/document_analysis.rb +75 -0
  12. data/lib/vanken/app/document_jobs.rb +52 -9
  13. data/lib/vanken/app/expert_info.rb +26 -0
  14. data/lib/vanken/app/io_graph.rb +47 -0
  15. data/lib/vanken/app/navigation.rb +77 -0
  16. data/lib/vanken/app/search_job.rb +62 -0
  17. data/lib/vanken/app/stats_job.rb +45 -0
  18. data/lib/vanken/capture/analyzer.rb +3 -1
  19. data/lib/vanken/capture/analyzer_process.rb +9 -6
  20. data/lib/vanken/capture/analyzer_worker.rb +2 -2
  21. data/lib/vanken/capture/filter_worker.rb +8 -2
  22. data/lib/vanken/capture/helper_main.rb +10 -3
  23. data/lib/vanken/capture/helper_options.rb +6 -2
  24. data/lib/vanken/capture/launcher.rb +1 -0
  25. data/lib/vanken/capture/permission_setup.rb +201 -0
  26. data/lib/vanken/config/analysis_settings.rb +58 -0
  27. data/lib/vanken/config/columns.rb +138 -0
  28. data/lib/vanken/config/messages.rb +105 -0
  29. data/lib/vanken/config/preferences.rb +34 -8
  30. data/lib/vanken/config/profiles.rb +74 -0
  31. data/lib/vanken/config/sessions.rb +69 -0
  32. data/lib/vanken/config/yaml_file.rb +36 -0
  33. data/lib/vanken/core/coloring.rb +86 -0
  34. data/lib/vanken/core/frame_store.rb +2 -2
  35. data/lib/vanken/core/stores.rb +12 -0
  36. data/lib/vanken/gateway/detail_builder.rb +1 -1
  37. data/lib/vanken/gateway/display_capture_filter.rb +102 -0
  38. data/lib/vanken/gateway/dissector.rb +31 -6
  39. data/lib/vanken/gateway/exporter.rb +125 -0
  40. data/lib/vanken/gateway/file_writer.rb +26 -2
  41. data/lib/vanken/gateway/interfaces.rb +59 -0
  42. data/lib/vanken/gateway/resolver.rb +131 -0
  43. data/lib/vanken/gateway/statistics.rb +83 -0
  44. data/lib/vanken/gateway/stream.rb +159 -0
  45. data/lib/vanken/ui/actions.rb +54 -28
  46. data/lib/vanken/ui/analysis_dialogs.rb +371 -0
  47. data/lib/vanken/ui/application.rb +183 -9
  48. data/lib/vanken/ui/coloring_operations.rb +104 -0
  49. data/lib/vanken/ui/column_operations.rb +115 -0
  50. data/lib/vanken/ui/dialogs.rb +41 -26
  51. data/lib/vanken/ui/file_operations.rb +17 -12
  52. data/lib/vanken/ui/filter_operations.rb +43 -15
  53. data/lib/vanken/ui/main_view.rb +74 -29
  54. data/lib/vanken/ui/navigation_operations.rb +86 -0
  55. data/lib/vanken/ui/packet_source.rb +33 -1
  56. data/lib/vanken/ui/selection.rb +8 -2
  57. data/lib/vanken/ui/settings_operations.rb +233 -0
  58. data/lib/vanken/version.rb +1 -1
  59. data/packaging/README.md +41 -1
  60. data/packaging/linux/org.vanken.capture.policy +14 -0
  61. data/packaging/linux/vanken-capture.wrapper +6 -0
  62. data/packaging/linux/vanken.desktop +9 -0
  63. data/packaging/linux/vanken.sudoers +2 -0
  64. data/packaging/macos/chmod-bpf +6 -0
  65. data/packaging/macos/org.vanken.chmod-bpf.plist +8 -0
  66. data/sig/generated/vanken/app/capture_controller.rbs +16 -0
  67. data/sig/generated/vanken/app/document.rbs +23 -1
  68. data/sig/generated/vanken/app/document_analysis.rbs +21 -0
  69. data/sig/generated/vanken/app/document_jobs.rbs +6 -0
  70. data/sig/generated/vanken/app/expert_info.rbs +30 -0
  71. data/sig/generated/vanken/app/io_graph.rbs +45 -0
  72. data/sig/generated/vanken/app/navigation.rbs +31 -0
  73. data/sig/generated/vanken/app/search_job.rbs +21 -0
  74. data/sig/generated/vanken/app/stats_job.rbs +18 -0
  75. data/sig/generated/vanken/capture/helper_options.rbs +6 -0
  76. data/sig/generated/vanken/capture/permission_setup.rbs +43 -0
  77. data/sig/generated/vanken/config/analysis_settings.rbs +31 -0
  78. data/sig/generated/vanken/config/columns.rbs +37 -0
  79. data/sig/generated/vanken/config/messages.rbs +14 -0
  80. data/sig/generated/vanken/config/preferences.rbs +1 -1
  81. data/sig/generated/vanken/config/profiles.rbs +29 -0
  82. data/sig/generated/vanken/config/sessions.rbs +17 -0
  83. data/sig/generated/vanken/config/yaml_file.rbs +11 -0
  84. data/sig/generated/vanken/core/coloring.rbs +52 -0
  85. data/sig/generated/vanken/core/stores.rbs +4 -0
  86. data/sig/generated/vanken/gateway/display_capture_filter.rbs +37 -0
  87. data/sig/generated/vanken/gateway/dissector.rbs +15 -2
  88. data/sig/generated/vanken/gateway/exporter.rbs +27 -0
  89. data/sig/generated/vanken/gateway/file_writer.rbs +3 -0
  90. data/sig/generated/vanken/gateway/interfaces.rbs +12 -0
  91. data/sig/generated/vanken/gateway/resolver.rbs +52 -0
  92. data/sig/generated/vanken/gateway/statistics.rbs +104 -0
  93. data/sig/generated/vanken/gateway/stream.rbs +75 -0
  94. data/sig/generated/vanken/ui/analysis_dialogs.rbs +71 -0
  95. data/sig/generated/vanken/ui/application.rbs +23 -1
  96. data/sig/generated/vanken/ui/coloring_operations.rbs +29 -0
  97. data/sig/generated/vanken/ui/column_operations.rbs +29 -0
  98. data/sig/generated/vanken/ui/dialogs.rbs +5 -3
  99. data/sig/generated/vanken/ui/file_operations.rbs +2 -0
  100. data/sig/generated/vanken/ui/filter_operations.rbs +3 -1
  101. data/sig/generated/vanken/ui/main_view.rbs +14 -2
  102. data/sig/generated/vanken/ui/navigation_operations.rbs +43 -0
  103. data/sig/generated/vanken/ui/packet_source.rbs +4 -0
  104. data/sig/generated/vanken/ui/settings_operations.rbs +41 -0
  105. metadata +61 -3
@@ -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
- @details = File.open(File.join(@store.directory, "details.jsonl"), "w+b", 0o600)
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
- snapshot = current.merge(marked: current[:marked].select { |n| n.between?(first, last) }.to_set,
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: nil,
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 = (@annotations.flush; @analyzed = true; notify(force: true))
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
- return self if @closed
241
- @closing = true
242
- cancel
243
- cancel_scan
244
- wait(3)
245
- @annotations.close
246
- @details.close
247
- @detail_reader.close
248
- @detail_index.close
249
- @summaries.close
250
- @summary_index.close
251
- @summary_reader.close
252
- @summary_index_reader.close
253
- @store.close
254
- @closed = true
255
- self
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 = 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