vanken 0.1.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 (104) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +5 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +84 -0
  5. data/docs/filters.md +25 -0
  6. data/docs/performance.md +113 -0
  7. data/docs/releases.md +15 -0
  8. data/docs/upstream.md +32 -0
  9. data/exe/vanken +5 -0
  10. data/exe/vanken-capture +6 -0
  11. data/lib/vanken/app/capture_controller.rb +197 -0
  12. data/lib/vanken/app/document.rb +305 -0
  13. data/lib/vanken/app/document_jobs.rb +199 -0
  14. data/lib/vanken/capture/analyzer.rb +44 -0
  15. data/lib/vanken/capture/analyzer_process.rb +91 -0
  16. data/lib/vanken/capture/analyzer_wire.rb +42 -0
  17. data/lib/vanken/capture/analyzer_worker.rb +153 -0
  18. data/lib/vanken/capture/control_protocol.rb +32 -0
  19. data/lib/vanken/capture/filter_worker.rb +180 -0
  20. data/lib/vanken/capture/helper_main.rb +137 -0
  21. data/lib/vanken/capture/helper_options.rb +70 -0
  22. data/lib/vanken/capture/launcher.rb +181 -0
  23. data/lib/vanken/capture/privileges.rb +24 -0
  24. data/lib/vanken/capture/receiver.rb +39 -0
  25. data/lib/vanken/cli.rb +68 -0
  26. data/lib/vanken/config/log.rb +19 -0
  27. data/lib/vanken/config/preferences.rb +126 -0
  28. data/lib/vanken/core/display_filter/compiler.rb +108 -0
  29. data/lib/vanken/core/display_filter/errors.rb +19 -0
  30. data/lib/vanken/core/display_filter/field_resolver.rb +86 -0
  31. data/lib/vanken/core/display_filter/lexer.rb +124 -0
  32. data/lib/vanken/core/display_filter/parser.rb +157 -0
  33. data/lib/vanken/core/display_filter/types.rb +176 -0
  34. data/lib/vanken/core/frame.rb +33 -0
  35. data/lib/vanken/core/frame_store.rb +179 -0
  36. data/lib/vanken/core/frame_view.rb +68 -0
  37. data/lib/vanken/core/stores.rb +192 -0
  38. data/lib/vanken/errors.rb +9 -0
  39. data/lib/vanken/gateway/capture_filter.rb +30 -0
  40. data/lib/vanken/gateway/detail_builder.rb +63 -0
  41. data/lib/vanken/gateway/dissector.rb +176 -0
  42. data/lib/vanken/gateway/file_reader.rb +153 -0
  43. data/lib/vanken/gateway/file_writer.rb +64 -0
  44. data/lib/vanken/gateway/interfaces.rb +30 -0
  45. data/lib/vanken/gateway/live_capture.rb +79 -0
  46. data/lib/vanken/gateway/packet_snapshot.rb +89 -0
  47. data/lib/vanken/ui/actions.rb +50 -0
  48. data/lib/vanken/ui/application.rb +205 -0
  49. data/lib/vanken/ui/dialogs.rb +89 -0
  50. data/lib/vanken/ui/file_operations.rb +113 -0
  51. data/lib/vanken/ui/filter_operations.rb +120 -0
  52. data/lib/vanken/ui/main_view.rb +113 -0
  53. data/lib/vanken/ui/packet_source.rb +161 -0
  54. data/lib/vanken/ui/selection.rb +91 -0
  55. data/lib/vanken/version.rb +5 -0
  56. data/lib/vanken.rb +17 -0
  57. data/packaging/README.md +33 -0
  58. data/sig/generated/vanken/app/capture_controller.rbs +58 -0
  59. data/sig/generated/vanken/app/document.rbs +117 -0
  60. data/sig/generated/vanken/app/document_jobs.rbs +27 -0
  61. data/sig/generated/vanken/capture/analyzer.rbs +11 -0
  62. data/sig/generated/vanken/capture/analyzer_process.rbs +17 -0
  63. data/sig/generated/vanken/capture/analyzer_wire.rbs +19 -0
  64. data/sig/generated/vanken/capture/analyzer_worker.rbs +45 -0
  65. data/sig/generated/vanken/capture/control_protocol.rbs +17 -0
  66. data/sig/generated/vanken/capture/filter_worker.rbs +51 -0
  67. data/sig/generated/vanken/capture/helper_main.rbs +23 -0
  68. data/sig/generated/vanken/capture/helper_options.rbs +27 -0
  69. data/sig/generated/vanken/capture/launcher.rbs +60 -0
  70. data/sig/generated/vanken/capture/privileges.rbs +12 -0
  71. data/sig/generated/vanken/capture/receiver.rbs +15 -0
  72. data/sig/generated/vanken/cli.rbs +9 -0
  73. data/sig/generated/vanken/config/log.rbs +9 -0
  74. data/sig/generated/vanken/config/preferences.rbs +49 -0
  75. data/sig/generated/vanken/core/display_filter/compiler.rbs +45 -0
  76. data/sig/generated/vanken/core/display_filter/errors.rbs +16 -0
  77. data/sig/generated/vanken/core/display_filter/field_resolver.rbs +48 -0
  78. data/sig/generated/vanken/core/display_filter/lexer.rbs +49 -0
  79. data/sig/generated/vanken/core/display_filter/parser.rbs +95 -0
  80. data/sig/generated/vanken/core/display_filter/types.rbs +45 -0
  81. data/sig/generated/vanken/core/frame.rbs +63 -0
  82. data/sig/generated/vanken/core/frame_store.rbs +65 -0
  83. data/sig/generated/vanken/core/frame_view.rbs +36 -0
  84. data/sig/generated/vanken/core/stores.rbs +110 -0
  85. data/sig/generated/vanken/errors.rbs +18 -0
  86. data/sig/generated/vanken/gateway/capture_filter.rbs +17 -0
  87. data/sig/generated/vanken/gateway/detail_builder.rbs +21 -0
  88. data/sig/generated/vanken/gateway/dissector.rbs +113 -0
  89. data/sig/generated/vanken/gateway/file_reader.rbs +53 -0
  90. data/sig/generated/vanken/gateway/file_writer.rbs +27 -0
  91. data/sig/generated/vanken/gateway/interfaces.rbs +16 -0
  92. data/sig/generated/vanken/gateway/live_capture.rbs +48 -0
  93. data/sig/generated/vanken/gateway/packet_snapshot.rbs +48 -0
  94. data/sig/generated/vanken/ui/actions.rbs +9 -0
  95. data/sig/generated/vanken/ui/application.rbs +85 -0
  96. data/sig/generated/vanken/ui/dialogs.rbs +21 -0
  97. data/sig/generated/vanken/ui/file_operations.rbs +25 -0
  98. data/sig/generated/vanken/ui/filter_operations.rbs +33 -0
  99. data/sig/generated/vanken/ui/main_view.rbs +21 -0
  100. data/sig/generated/vanken/ui/packet_source.rbs +40 -0
  101. data/sig/generated/vanken/ui/selection.rbs +23 -0
  102. data/sig/generated/vanken/version.rbs +5 -0
  103. data/sig/vanken.rbs +25 -0
  104. metadata +192 -0
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "strscan"
4
+ require "ipaddr"
5
+ require_relative "errors"
6
+
7
+ module Vanken
8
+ module Core
9
+ module DisplayFilter
10
+ Token = Data.define(
11
+ :type, #: Symbol
12
+ :value, #: untyped
13
+ :position, #: Integer
14
+ :length, #: Integer
15
+ :raw #: String
16
+ )
17
+
18
+ class Lexer
19
+ OPERATORS = {
20
+ "==" => :eq, "eq" => :eq, "!=" => :ne, "ne" => :ne, "~=" => :any_ne, "any_ne" => :any_ne,
21
+ ">" => :gt, "gt" => :gt, "<" => :lt, "lt" => :lt, ">=" => :ge, "ge" => :ge,
22
+ "<=" => :le, "le" => :le, "contains" => :contains, "matches" => :matches, "in" => :in,
23
+ "&" => :bitmask, "and" => :and, "&&" => :and, "or" => :or, "||" => :or,
24
+ "not" => :not, "!" => :not
25
+ }.freeze
26
+ PUNCTUATION = {"(" => :lparen, ")" => :rparen, "{" => :lbrace, "}" => :rbrace,
27
+ "," => :comma, ".." => :range}.freeze
28
+
29
+ # @rbs (String expression) -> void
30
+ def initialize(expression)
31
+ raise SyntaxError.new("Filter exceeds 8192 bytes", position: 8192) if expression.bytesize > 8192
32
+ @scanner = StringScanner.new(expression)
33
+ end
34
+
35
+ # @rbs () -> Array[Token]
36
+ def tokens
37
+ result = [] #: Array[Token]
38
+ until @scanner.eos?
39
+ next if @scanner.scan(/\s+/)
40
+
41
+ position = @scanner.pos
42
+ if @scanner.peek(1) == '"'
43
+ value = read_string(position)
44
+ raw = @scanner.string.byteslice(position...@scanner.pos)
45
+ result << Token.new(:string, value, position, raw.bytesize, raw)
46
+ next
47
+ end
48
+ raw = @scanner.scan(/==|!=|~=|>=|<=|&&|\|\||\.\.|[(){}<>&!,]/) ||
49
+ @scanner.scan(/(?:(?!\.\.)[^\s(){}<>=!~&|,"'])+/)
50
+ fail_at("Unexpected character", position, 1) unless raw
51
+ type, value = classify(raw, position)
52
+ result << Token.new(type, value, position, raw.bytesize, raw)
53
+ end
54
+ result << Token.new(:eof, nil, @scanner.pos, 0, "")
55
+ end
56
+
57
+ private
58
+
59
+ # @rbs (Integer position) -> String
60
+ def read_string(position)
61
+ @scanner.getch
62
+ value = +"".b
63
+ until @scanner.eos?
64
+ character = @scanner.getch
65
+ if character == '"'
66
+ value.force_encoding(@scanner.string.encoding)
67
+ return value.valid_encoding? ? value : value.b
68
+ end
69
+ if character == "\\"
70
+ escaped = @scanner.getch
71
+ if escaped == "x"
72
+ hex = @scanner.scan(/[0-9a-fA-F]{2}/)
73
+ fail_at("Expected two hexadecimal escape digits", position, @scanner.pos - position) unless hex
74
+ value << hex.to_i(16).chr
75
+ elsif escaped == '"' || escaped == "\\"
76
+ value << escaped
77
+ else
78
+ fail_at("Unknown string escape", position, @scanner.pos - position)
79
+ end
80
+ else
81
+ value << character.b
82
+ end
83
+ end
84
+ fail_at("Unterminated string", position, @scanner.pos - position)
85
+ end
86
+
87
+ # @rbs (String raw, Integer position) -> [Symbol, untyped]
88
+ def classify(raw, position)
89
+ return [:operator, OPERATORS.fetch(raw)] if OPERATORS.key?(raw)
90
+ return [PUNCTUATION.fetch(raw), raw] if PUNCTUATION.key?(raw)
91
+ return [:boolean, raw == "true"] if %w[true false].include?(raw)
92
+ if /\A-?(?:0x[\da-f]+|0o[0-7]+|0b[01]+|\d+)\z/i.match?(raw)
93
+ return [:integer, Integer(raw, raw.match?(/\A-?0[xob]/i) ? 0 : 10)]
94
+ end
95
+ if /\A-?(?:\d+\.\d+(?:e[+-]?\d+)?|\d+e[+-]?\d+)\z/i.match?(raw)
96
+ value = Float(raw)
97
+ fail_at("Number must be finite", position, raw.bytesize) unless value.finite?
98
+ return [:float, value]
99
+ end
100
+ return [:mac, raw] if /\A(?:[\da-f]{2}:){5}[\da-f]{2}\z/i.match?(raw) ||
101
+ /\A(?:[\da-f]{2}-){5}[\da-f]{2}\z/i.match?(raw) ||
102
+ /\A[\da-f]{4}\.[\da-f]{4}\.[\da-f]{4}\z/i.match?(raw)
103
+ return [:bytes, raw] if /\A(?:[\da-f]{2}:)+[\da-f]{2}\z/i.match?(raw) && raw.count(":") != 7
104
+ if raw.include?(":") || /\A\d+\./.match?(raw)
105
+ begin
106
+ IPAddr.new(raw)
107
+ return [:address, raw]
108
+ rescue IPAddr::Error
109
+ fail_at("Invalid address", position, raw.bytesize)
110
+ end
111
+ end
112
+ return [:identifier, raw] if /\A[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*\z/.match?(raw)
113
+
114
+ fail_at("Invalid token #{raw.inspect}", position, raw.bytesize)
115
+ end
116
+
117
+ # @rbs (String message, Integer position, Integer length) -> bot
118
+ def fail_at(message, position, length)
119
+ raise SyntaxError.new(message, position: position, length: length)
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lexer"
4
+
5
+ module Vanken
6
+ module Core
7
+ module DisplayFilter
8
+ Node = Data.define(
9
+ :kind, #: Symbol
10
+ :left, #: untyped
11
+ :right #: untyped
12
+ )
13
+ Predicate = Data.define(
14
+ :operator, #: Symbol
15
+ :values, #: Array[Member]
16
+ :token #: Token
17
+ )
18
+ Member = Data.define(
19
+ :first, #: Token
20
+ :last #: Token?
21
+ )
22
+
23
+ class Parser
24
+ PRECEDENCE = {or: 10, and: 20}.freeze
25
+ VALUES = %i[integer float string address mac bytes boolean identifier].freeze
26
+ RELATIONS = %i[eq ne any_ne gt lt ge le contains].freeze
27
+ attr_reader :warnings #: Array[String]
28
+
29
+ # @rbs (String expression) -> void
30
+ def initialize(expression)
31
+ @tokens = Lexer.new(expression).tokens
32
+ raise SyntaxError.new("Filter has too many tokens", position: @tokens[512].position) if @tokens.size > 512
33
+ @index = 0
34
+ @warnings = []
35
+ @scopes = [[]] #: Array[Array[Symbol]]
36
+ @scope = 0
37
+ @depth = 0
38
+ end
39
+
40
+ # @rbs () -> Node
41
+ def parse
42
+ return Node.new(:true, nil, nil) if current.type == :eof
43
+
44
+ ast = expression(0)
45
+ expect_token(:eof)
46
+ if @scopes.any? { |operators| operators.uniq.size > 1 }
47
+ @warnings << "Use parentheses when mixing and with or"
48
+ end
49
+ ast
50
+ end
51
+
52
+ private
53
+
54
+ # @rbs (Integer minimum) -> Node
55
+ def expression(minimum)
56
+ left = primary
57
+ while current.type == :operator && (precedence = PRECEDENCE[current.value]) && precedence >= minimum
58
+ operator = advance.value
59
+ @scopes[@scope] << operator
60
+ left = Node.new(operator, left, expression(precedence + 1))
61
+ end
62
+ left
63
+ end
64
+
65
+ # @rbs () -> Node
66
+ def primary
67
+ @depth += 1
68
+ fail_at("Filter nesting exceeds 64 levels", current) if @depth > 64
69
+ if current.type == :operator && current.value == :not
70
+ advance
71
+ return Node.new(:not, primary, nil)
72
+ end
73
+ if current.type == :lparen
74
+ advance
75
+ parent_scope = @scope
76
+ @scope = @scopes.length
77
+ scope = [] #: Array[Symbol]
78
+ @scopes << scope
79
+ node = expression(0)
80
+ expect_token(:rparen)
81
+ @scope = parent_scope
82
+ return node
83
+ end
84
+ field = expect_token(:identifier)
85
+ Node.new(:test, field, predicate)
86
+ ensure
87
+ @depth -= 1
88
+ end
89
+
90
+ # @rbs () -> Predicate?
91
+ def predicate
92
+ return nil unless current.type == :operator
93
+ return nil if PRECEDENCE.key?(current.value)
94
+
95
+ operator = advance
96
+ if RELATIONS.include?(operator.value)
97
+ members = [Member.new(value, nil)]
98
+ elsif operator.value == :matches
99
+ members = [Member.new(expect_token(:string), nil)]
100
+ elsif operator.value == :bitmask
101
+ members = [Member.new(expect_token(:integer), nil)]
102
+ elsif operator.value == :in
103
+ members = set_members
104
+ else
105
+ fail_at("Unexpected operator", operator)
106
+ end
107
+ Predicate.new(operator.value, members, operator)
108
+ end
109
+
110
+ # @rbs () -> Array[Member]
111
+ def set_members
112
+ expect_token(:lbrace)
113
+ members = [] #: Array[Member]
114
+ loop do
115
+ first = value
116
+ last = current.type == :range ? (advance; value) : nil
117
+ members << Member.new(first, last)
118
+ break if current.type == :rbrace
119
+
120
+ advance if current.type == :comma
121
+ end
122
+ expect_token(:rbrace)
123
+ members
124
+ end
125
+
126
+ # @rbs () -> Token
127
+ def value
128
+ return advance if VALUES.include?(current.type)
129
+
130
+ fail_at("Expected a value", current)
131
+ end
132
+
133
+ # @rbs (Symbol type) -> Token
134
+ def expect_token(type)
135
+ return advance if current.type == type
136
+
137
+ fail_at("Expected #{type}", current)
138
+ end
139
+
140
+ # @rbs () -> Token
141
+ def current = @tokens.fetch(@index)
142
+
143
+ # @rbs () -> Token
144
+ def advance
145
+ token = current
146
+ @index += 1
147
+ token
148
+ end
149
+
150
+ # @rbs (String message, Token token) -> bot
151
+ def fail_at(message, token)
152
+ raise SyntaxError.new(message, position: token.position, length: token.length)
153
+ end
154
+ end
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,176 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ipaddr"
4
+
5
+ module Vanken
6
+ module Core
7
+ module DisplayFilter
8
+ module Types
9
+ NUMERIC = %i[integer float].freeze
10
+ ORDERED = %i[integer float severity].freeze
11
+ SEVERITIES = {"note" => 0, "warning" => 1, "warn" => 1, "error" => 2}.freeze
12
+ module_function
13
+
14
+ # @rbs (Predicate predicate, field_type? type) -> ^(untyped, field_type?) -> bool
15
+ def predicate(predicate, type)
16
+ operator = predicate.operator
17
+ validate_operator(operator, type, predicate.token)
18
+ regex = regular_expression(predicate.values.first.first) if operator == :matches
19
+ members = prepare_members(predicate.values, type) if type
20
+ lambda do |actual, runtime_type|
21
+ value_type = type || runtime_type || infer_type(actual, predicate.values.first.first)
22
+ validate_operator(operator, value_type, predicate.token) unless type
23
+ prepared = members || prepare_members(predicate.values, value_type)
24
+ value = actual_value(actual, value_type)
25
+ compare(value, operator, prepared, regex, value_type)
26
+ rescue ArgumentError, TypeError, Regexp::TimeoutError, SyntaxError
27
+ false
28
+ end
29
+ end
30
+
31
+ # @rbs (Symbol operator, field_type? type, Token token) -> void
32
+ def validate_operator(operator, type, token)
33
+ return unless type
34
+
35
+ allowed = case operator
36
+ when :bitmask then type == :integer
37
+ when :contains then %i[string bytes].include?(type)
38
+ when :matches then type == :string
39
+ when :gt, :lt, :ge, :le then ORDERED.include?(type)
40
+ else true
41
+ end
42
+ fail_at("#{operator} is not supported for #{type}", token) unless allowed
43
+ end
44
+
45
+ # @rbs (Array[Member] members, field_type? type) -> Array[prepared_member]
46
+ def prepare_members(members, type)
47
+ members.map do |member|
48
+ first = literal(member.first, type)
49
+ last = literal(member.last, type) if member.last
50
+ if member.last && (!ORDERED.include?(type) || first > last)
51
+ fail_at("Invalid range", member.last)
52
+ end
53
+ [first, last]
54
+ end
55
+ end
56
+
57
+ # @rbs (Token token, field_type? type) -> untyped
58
+ def literal(token, type)
59
+ case type
60
+ when :integer, :float
61
+ fail_at("Expected a numeric value", token) unless NUMERIC.include?(token.type)
62
+ token.value
63
+ when :boolean
64
+ return token.value if token.type == :boolean
65
+ return token.value == 1 if token.type == :integer && [0, 1].include?(token.value)
66
+
67
+ fail_at("Expected true, false, 1, or 0", token)
68
+ when :address
69
+ IPAddr.new(token.value.to_s)
70
+ when :mac
71
+ normalized_mac(token.value)
72
+ when :bytes
73
+ return [token.raw.delete_prefix("0x").delete_prefix("0X")].pack("H*") if token.type == :integer && token.raw.match?(/\A0x(?:[\da-f]{2})+\z/i)
74
+ return [token.value.delete(".:-")].pack("H*") if %i[bytes mac].include?(token.type) ||
75
+ (token.type == :address && /\A(?:[\da-f]{2}:)+[\da-f]{2}\z/i.match?(token.raw))
76
+ return token.value.b if token.type == :string
77
+
78
+ fail_at("Expected bytes or a string", token)
79
+ when :severity
80
+ SEVERITIES.fetch(token.value.to_s) { fail_at("Expected note, warning, or error", token) }
81
+ when :string
82
+ fail_at("Expected a string", token) unless %i[string identifier].include?(token.type)
83
+ token.value
84
+ else
85
+ token.type == :address ? IPAddr.new(token.value) : token.value
86
+ end
87
+ rescue ArgumentError
88
+ fail_at("Invalid #{type} value", token)
89
+ end
90
+
91
+ # @rbs (untyped value, field_type? type) -> untyped
92
+ def actual_value(value, type)
93
+ case type
94
+ when :address then value.is_a?(IPAddr) ? value : IPAddr.new(value.to_s)
95
+ when :mac then normalized_mac(value)
96
+ when :bytes then value.b
97
+ when :severity then SEVERITIES.fetch(value.to_s)
98
+ when :string then value.to_s
99
+ when :boolean
100
+ return value if value == true || value == false
101
+ return value == 1 if value == 0 || value == 1
102
+
103
+ raise TypeError, "Invalid boolean value"
104
+ when :integer, :float
105
+ raise TypeError, "Invalid numeric value" unless value.is_a?(Numeric)
106
+ value
107
+ else value
108
+ end
109
+ end
110
+
111
+ # @rbs (untyped actual, Token token) -> field_type
112
+ def infer_type(actual, token)
113
+ return :boolean if actual == true || actual == false
114
+ return :integer if actual.is_a?(Integer)
115
+ return :float if actual.is_a?(Numeric)
116
+ return :address if actual.is_a?(IPAddr) || token.type == :address
117
+ return :mac if token.type == :mac
118
+ return :bytes if token.type == :bytes || (actual.is_a?(String) && actual.encoding == Encoding::BINARY)
119
+
120
+ :string
121
+ end
122
+
123
+ # @rbs (untyped value, Symbol operator, Array[prepared_member] members, Regexp? regex, field_type? type) -> bool
124
+ def compare(value, operator, members, regex, type)
125
+ expected = members.first.first
126
+ case operator
127
+ when :eq, :ne, :any_ne
128
+ equal = type == :address ? expected.include?(value) : value == expected
129
+ operator == :eq ? equal : !equal
130
+ when :gt then value > expected
131
+ when :lt then value < expected
132
+ when :ge then value >= expected
133
+ when :le then value <= expected
134
+ when :contains then value.include?(expected)
135
+ when :matches then !!regex&.match?(value)
136
+ when :bitmask then (value & expected) != 0
137
+ when :in
138
+ members.any? do |first, last|
139
+ if last
140
+ value >= first && value <= last
141
+ elsif type == :address
142
+ first.include?(value)
143
+ else
144
+ value == first
145
+ end
146
+ end
147
+ else raise ArgumentError, "Unknown filter operator: #{operator}"
148
+ end
149
+ end
150
+
151
+ # @rbs (untyped value) -> String
152
+ def normalized_mac(value)
153
+ string = value.to_s
154
+ valid = /\A(?:[\da-f]{2}:){5}[\da-f]{2}\z/i.match?(string) ||
155
+ /\A(?:[\da-f]{2}-){5}[\da-f]{2}\z/i.match?(string) ||
156
+ /\A[\da-f]{4}\.[\da-f]{4}\.[\da-f]{4}\z/i.match?(string)
157
+ raise ArgumentError, "Invalid MAC address" unless valid
158
+
159
+ string.delete(".:-").downcase
160
+ end
161
+
162
+ # @rbs (Token token) -> Regexp
163
+ def regular_expression(token)
164
+ Regexp.new(token.value, timeout: 0.1)
165
+ rescue RegexpError
166
+ fail_at("Invalid regular expression", token)
167
+ end
168
+
169
+ # @rbs (String message, Token token) -> bot
170
+ def fail_at(message, token)
171
+ raise SyntaxError.new(message, position: token.position, length: token.length)
172
+ end
173
+ end
174
+ end
175
+ end
176
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ module Vanken
5
+ module Core
6
+ Frame = Data.define(
7
+ :bytes, #: String
8
+ :timestamp_ns, #: Integer
9
+ :original_length, #: Integer
10
+ :linktype, #: Integer
11
+ :interface, #: Hash[String, untyped]?
12
+ :direction, #: (:in | :out)?
13
+ :number #: Integer
14
+ )
15
+ DetailNode = Data.define(
16
+ :id, #: String
17
+ :label, #: String
18
+ :field, #: String?
19
+ :offset, #: Integer?
20
+ :length, #: Integer
21
+ :source, #: Symbol
22
+ :severity, #: Symbol?
23
+ :filter, #: String?
24
+ :children #: Array[DetailNode]
25
+ )
26
+ class DetailNode
27
+ # @rbs () -> Array[DetailNode]
28
+ def descendants = [self, *children.flat_map(&:descendants)]
29
+ # @rbs () -> Hash[Symbol, untyped]
30
+ def to_h = super.merge(children: children.map(&:to_h))
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,179 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ require "tmpdir"
5
+ require "json"
6
+ require "fileutils"
7
+ require_relative "frame"
8
+
9
+ module Vanken
10
+ module Core
11
+ class FrameStore
12
+ RECORD = "Q<L<L<q<S<S<Cx3"
13
+ RECORD_SIZE = 32
14
+ DIRECTION_IDS = {nil => 0, in: 1, out: 2}.freeze #: Hash[Symbol?, Integer]
15
+ MIN_TIMESTAMP = -(1 << 63) #: Integer
16
+ MAX_TIMESTAMP = (1 << 63) - 1 #: Integer
17
+ attr_reader :directory #: String
18
+
19
+ # @rbs! @interfaces: Array[Hash[String, untyped]]
20
+
21
+ # @rbs (?parent: String?, ?directory: String?, ?recover: bool) -> void
22
+ def initialize(parent: nil, directory: nil, recover: false)
23
+ @directory = directory || Dir.mktmpdir("vanken-", parent)
24
+ validate_directory
25
+ File.chmod(0o700, @directory)
26
+ @mutex = Mutex.new
27
+ @count = 0
28
+ @interfaces = []
29
+ @durable_count = 0
30
+ @closed = false
31
+ restore if recover
32
+ @data = private_file("frames.bin", recover ? "r+b" : "w+b")
33
+ @records = private_file("frames.idx", recover ? "r+b" : "w+b")
34
+ @data.seek(0, IO::SEEK_END)
35
+ @records.seek(0, IO::SEEK_END)
36
+ @reader = File.open(File.join(@directory, "frames.bin"), "rb")
37
+ @index_reader = File.open(File.join(@directory, "frames.idx"), "rb")
38
+ persist("session.json", {"schema_version" => 1, "created_at" => Time.now.utc.iso8601, "pid" => Process.pid})
39
+ end
40
+
41
+ # @rbs (String directory) -> FrameStore
42
+ def self.recover(directory) = new(directory: directory, recover: true)
43
+ # @rbs () -> Integer
44
+ def count = @mutex.synchronize { @count }
45
+ # @rbs () -> Integer
46
+ # Published only after both files flush; reading the immutable count must not wait for writer IO.
47
+ def durable_count = @durable_count
48
+
49
+ # @rbs (Frame frame) -> Integer
50
+ def append(frame)
51
+ raise ArgumentError, "invalid frame lengths" unless frame.bytes.is_a?(String) && frame.original_length.is_a?(Integer) && frame.original_length.between?(frame.bytes.bytesize, 0xffff_ffff) && frame.bytes.bytesize <= 16 << 20
52
+ raise ArgumentError, "invalid frame metadata" unless frame.linktype.is_a?(Integer) && frame.linktype.between?(0, 0xffff) && frame.timestamp_ns.is_a?(Integer) && frame.timestamp_ns.between?(MIN_TIMESTAMP, MAX_TIMESTAMP)
53
+ @mutex.synchronize do
54
+ raise IOError, "store closed" if @closed
55
+ interface = frame.interface ? intern_interface(frame.interface) : 0xffff
56
+ direction = DIRECTION_IDS.fetch(frame.direction)
57
+ record = [@data.pos, frame.bytes.bytesize, frame.original_length, frame.timestamp_ns,
58
+ frame.linktype, interface, direction].pack(RECORD)
59
+ @data.write(frame.bytes)
60
+ @records.write(record)
61
+ @count += 1
62
+ end
63
+ end
64
+
65
+ # @rbs () -> self
66
+ def flush
67
+ @mutex.synchronize do
68
+ @data.flush
69
+ @records.flush
70
+ @durable_count = @count
71
+ end
72
+ self
73
+ end
74
+
75
+ # @rbs (Integer number) -> frame_metadata
76
+ def metadata(number)
77
+ @mutex.synchronize do
78
+ raise IndexError, "frame is not durable" unless number.is_a?(Integer) && number.between?(1, @durable_count)
79
+ offset = (number - 1) * RECORD_SIZE
80
+ record = if @index_reader.respond_to?(:pread)
81
+ @index_reader.pread(RECORD_SIZE, offset)
82
+ else
83
+ @index_reader.seek(offset)
84
+ @index_reader.read(RECORD_SIZE)
85
+ end
86
+ raise IOError, "truncated frame index" unless record && record.bytesize == RECORD_SIZE
87
+ values = record.unpack(RECORD)
88
+ {offset: values[0], caplen: values[1], original_length: values[2], timestamp_ns: values[3],
89
+ linktype: values[4], interface: values[5] == 0xffff ? nil : @interfaces.fetch(values[5]),
90
+ direction: [nil, :in, :out].fetch(values[6]), number: number}
91
+ end
92
+ end
93
+
94
+ # @rbs (Integer number) -> Frame
95
+ def read(number)
96
+ meta = metadata(number)
97
+ bytes = if @reader.respond_to?(:pread)
98
+ meta[:caplen].zero? ? "".b : @reader.pread(meta[:caplen], meta[:offset])
99
+ else
100
+ @mutex.synchronize { @reader.seek(meta[:offset]); @reader.read(meta[:caplen]) }
101
+ end
102
+ raise IOError, "truncated frame spool" unless bytes && bytes.bytesize == meta[:caplen]
103
+ Frame.new(bytes: bytes, timestamp_ns: meta[:timestamp_ns], original_length: meta[:original_length],
104
+ linktype: meta[:linktype], interface: meta[:interface], direction: meta[:direction], number: number)
105
+ end
106
+
107
+ # @rbs () -> Array[Hash[String, untyped]]
108
+ def interfaces = @mutex.synchronize { @interfaces.dup }
109
+
110
+ # @rbs (?remove: bool) -> void
111
+ def close(remove: true)
112
+ @mutex.synchronize do
113
+ unless @closed
114
+ [@data, @records, @reader, @index_reader].compact.each(&:close)
115
+ @closed = true
116
+ end
117
+ end
118
+ FileUtils.remove_entry_secure(@directory) if remove && File.directory?(@directory)
119
+ end
120
+
121
+ private
122
+
123
+ # @rbs (String name, String mode) -> File
124
+ def private_file(name, mode)
125
+ path = File.join(@directory, name)
126
+ io = File.open(path, mode, 0o600)
127
+ io.chmod(0o600)
128
+ io
129
+ end
130
+
131
+ def validate_directory
132
+ raise Vanken::FileError, "unsafe session directory" unless File.directory?(@directory) && !File.symlink?(@directory) && File.stat(@directory).uid == Process.uid
133
+ %w[frames.idx frames.bin interfaces.json session.json].each do |name|
134
+ path = File.join(@directory, name)
135
+ next unless File.exist?(path) || File.symlink?(path)
136
+ stat = File.lstat(path)
137
+ raise Vanken::FileError, "unsafe session file" unless stat.file? && stat.uid == Process.uid && stat.nlink == 1
138
+ end
139
+ end
140
+
141
+ def persist(name, value)
142
+ File.open(File.join(@directory, name), "w", 0o600) { |io| io.write(JSON.generate(value)) }
143
+ File.chmod(0o600, File.join(@directory, name))
144
+ end
145
+
146
+ def intern_interface(value)
147
+ value = value.transform_keys(&:to_s)
148
+ found = @interfaces.index(value)
149
+ return found if found
150
+ raise ArgumentError, "too many interfaces" if @interfaces.length >= 0xffff
151
+ @interfaces << value.freeze
152
+ persist("interfaces.json", @interfaces)
153
+ @interfaces.length - 1
154
+ end
155
+
156
+ def restore
157
+ raise Vanken::FileError, "unsafe session directory" unless File.directory?(@directory) && !File.symlink?(@directory) && File.stat(@directory).uid == Process.uid
158
+ %w[frames.idx frames.bin interfaces.json].each do |name|
159
+ raise Vanken::FileError, "unsafe session file" if File.symlink?(File.join(@directory, name))
160
+ end
161
+ path = File.join(@directory, "frames.idx")
162
+ limit = File.size(File.join(@directory, "frames.bin"))
163
+ @interfaces = JSON.parse(File.read(File.join(@directory, "interfaces.json"))) if File.exist?(File.join(@directory, "interfaces.json"))
164
+ expected_offset = 0
165
+ File.open(path, "rb") do |io|
166
+ while (record = io.read(RECORD_SIZE)) && record.bytesize == RECORD_SIZE
167
+ offset, caplen, original, _time, _link, interface, direction = record.unpack(RECORD)
168
+ break unless offset == expected_offset && caplen <= 16 << 20 && offset + caplen <= limit && original >= caplen && (interface == 0xffff || interface < @interfaces.size) && direction <= 2
169
+ @count += 1
170
+ expected_offset = offset + caplen
171
+ end
172
+ end
173
+ @durable_count = @count
174
+ File.truncate(path, @count * RECORD_SIZE)
175
+ File.truncate(File.join(@directory, "frames.bin"), expected_offset)
176
+ end
177
+ end
178
+ end
179
+ end