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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +84 -0
- data/docs/filters.md +25 -0
- data/docs/performance.md +113 -0
- data/docs/releases.md +15 -0
- data/docs/upstream.md +32 -0
- data/exe/vanken +5 -0
- data/exe/vanken-capture +6 -0
- data/lib/vanken/app/capture_controller.rb +197 -0
- data/lib/vanken/app/document.rb +305 -0
- data/lib/vanken/app/document_jobs.rb +199 -0
- data/lib/vanken/capture/analyzer.rb +44 -0
- data/lib/vanken/capture/analyzer_process.rb +91 -0
- data/lib/vanken/capture/analyzer_wire.rb +42 -0
- data/lib/vanken/capture/analyzer_worker.rb +153 -0
- data/lib/vanken/capture/control_protocol.rb +32 -0
- data/lib/vanken/capture/filter_worker.rb +180 -0
- data/lib/vanken/capture/helper_main.rb +137 -0
- data/lib/vanken/capture/helper_options.rb +70 -0
- data/lib/vanken/capture/launcher.rb +181 -0
- data/lib/vanken/capture/privileges.rb +24 -0
- data/lib/vanken/capture/receiver.rb +39 -0
- data/lib/vanken/cli.rb +68 -0
- data/lib/vanken/config/log.rb +19 -0
- data/lib/vanken/config/preferences.rb +126 -0
- data/lib/vanken/core/display_filter/compiler.rb +108 -0
- data/lib/vanken/core/display_filter/errors.rb +19 -0
- data/lib/vanken/core/display_filter/field_resolver.rb +86 -0
- data/lib/vanken/core/display_filter/lexer.rb +124 -0
- data/lib/vanken/core/display_filter/parser.rb +157 -0
- data/lib/vanken/core/display_filter/types.rb +176 -0
- data/lib/vanken/core/frame.rb +33 -0
- data/lib/vanken/core/frame_store.rb +179 -0
- data/lib/vanken/core/frame_view.rb +68 -0
- data/lib/vanken/core/stores.rb +192 -0
- data/lib/vanken/errors.rb +9 -0
- data/lib/vanken/gateway/capture_filter.rb +30 -0
- data/lib/vanken/gateway/detail_builder.rb +63 -0
- data/lib/vanken/gateway/dissector.rb +176 -0
- data/lib/vanken/gateway/file_reader.rb +153 -0
- data/lib/vanken/gateway/file_writer.rb +64 -0
- data/lib/vanken/gateway/interfaces.rb +30 -0
- data/lib/vanken/gateway/live_capture.rb +79 -0
- data/lib/vanken/gateway/packet_snapshot.rb +89 -0
- data/lib/vanken/ui/actions.rb +50 -0
- data/lib/vanken/ui/application.rb +205 -0
- data/lib/vanken/ui/dialogs.rb +89 -0
- data/lib/vanken/ui/file_operations.rb +113 -0
- data/lib/vanken/ui/filter_operations.rb +120 -0
- data/lib/vanken/ui/main_view.rb +113 -0
- data/lib/vanken/ui/packet_source.rb +161 -0
- data/lib/vanken/ui/selection.rb +91 -0
- data/lib/vanken/version.rb +5 -0
- data/lib/vanken.rb +17 -0
- data/packaging/README.md +33 -0
- data/sig/generated/vanken/app/capture_controller.rbs +58 -0
- data/sig/generated/vanken/app/document.rbs +117 -0
- data/sig/generated/vanken/app/document_jobs.rbs +27 -0
- data/sig/generated/vanken/capture/analyzer.rbs +11 -0
- data/sig/generated/vanken/capture/analyzer_process.rbs +17 -0
- data/sig/generated/vanken/capture/analyzer_wire.rbs +19 -0
- data/sig/generated/vanken/capture/analyzer_worker.rbs +45 -0
- data/sig/generated/vanken/capture/control_protocol.rbs +17 -0
- data/sig/generated/vanken/capture/filter_worker.rbs +51 -0
- data/sig/generated/vanken/capture/helper_main.rbs +23 -0
- data/sig/generated/vanken/capture/helper_options.rbs +27 -0
- data/sig/generated/vanken/capture/launcher.rbs +60 -0
- data/sig/generated/vanken/capture/privileges.rbs +12 -0
- data/sig/generated/vanken/capture/receiver.rbs +15 -0
- data/sig/generated/vanken/cli.rbs +9 -0
- data/sig/generated/vanken/config/log.rbs +9 -0
- data/sig/generated/vanken/config/preferences.rbs +49 -0
- data/sig/generated/vanken/core/display_filter/compiler.rbs +45 -0
- data/sig/generated/vanken/core/display_filter/errors.rbs +16 -0
- data/sig/generated/vanken/core/display_filter/field_resolver.rbs +48 -0
- data/sig/generated/vanken/core/display_filter/lexer.rbs +49 -0
- data/sig/generated/vanken/core/display_filter/parser.rbs +95 -0
- data/sig/generated/vanken/core/display_filter/types.rbs +45 -0
- data/sig/generated/vanken/core/frame.rbs +63 -0
- data/sig/generated/vanken/core/frame_store.rbs +65 -0
- data/sig/generated/vanken/core/frame_view.rbs +36 -0
- data/sig/generated/vanken/core/stores.rbs +110 -0
- data/sig/generated/vanken/errors.rbs +18 -0
- data/sig/generated/vanken/gateway/capture_filter.rbs +17 -0
- data/sig/generated/vanken/gateway/detail_builder.rbs +21 -0
- data/sig/generated/vanken/gateway/dissector.rbs +113 -0
- data/sig/generated/vanken/gateway/file_reader.rbs +53 -0
- data/sig/generated/vanken/gateway/file_writer.rbs +27 -0
- data/sig/generated/vanken/gateway/interfaces.rbs +16 -0
- data/sig/generated/vanken/gateway/live_capture.rbs +48 -0
- data/sig/generated/vanken/gateway/packet_snapshot.rbs +48 -0
- data/sig/generated/vanken/ui/actions.rbs +9 -0
- data/sig/generated/vanken/ui/application.rbs +85 -0
- data/sig/generated/vanken/ui/dialogs.rbs +21 -0
- data/sig/generated/vanken/ui/file_operations.rbs +25 -0
- data/sig/generated/vanken/ui/filter_operations.rbs +33 -0
- data/sig/generated/vanken/ui/main_view.rbs +21 -0
- data/sig/generated/vanken/ui/packet_source.rbs +40 -0
- data/sig/generated/vanken/ui/selection.rbs +23 -0
- data/sig/generated/vanken/version.rbs +5 -0
- data/sig/vanken.rbs +25 -0
- 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
|